001/* 002 * Copyright (c) 2010-2024 Mark Allen, Norbert Bartels. 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining a copy 005 * of this software and associated documentation files (the "Software"), to deal 006 * in the Software without restriction, including without limitation the rights 007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 008 * copies of the Software, and to permit persons to whom the Software is 009 * furnished to do so, subject to the following conditions: 010 * 011 * The above copyright notice and this permission notice shall be included in 012 * all copies or substantial portions of the Software. 013 * 014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 020 * THE SOFTWARE. 021 */ 022package com.restfb; 023 024import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED; 025 026import java.io.IOException; 027import java.net.HttpURLConnection; 028import java.util.Collections; 029import java.util.Map; 030import java.util.function.Supplier; 031 032import com.restfb.util.SoftHashMap; 033 034/** 035 * WebRequestor with ETag-support. 036 * 037 * <p> 038 * The {@link ETagWebRequestor} caches all <tt>GET</tt>-requests with an ETag header field in a {@link SoftHashMap} and 039 * uses the ETag on the next request as <code>If-None-Match</code> header field if the same URL is requested. 040 * </p> 041 * 042 * <p> 043 * Is the response status code 304 (NOT MODIFIED) the old response from cache is used. 044 * </p> 045 * 046 * <p> 047 * <strong>Attention:</strong> even 304 responses count as request at Facebook and so they count against the throttling 048 * limits. Facebook suggests to use them for data that change only frequently 049 * </p> 050 * 051 * <p> 052 * Further information regarding ETag at Facebook can be found here: 053 * <a href="https://developers.facebook.com/blog/post/627/">https://developers.facebook.com/blog/post/627/</a> 054 * </p> 055 * 056 * <p> 057 * <strong>Attention 2</strong>: If excessively used with a lot of URLs, the {@link SoftHashMap} can lead to a 058 * performance degradation 059 * </p> 060 */ 061public class ETagWebRequestor extends DefaultWebRequestor { 062 063 private static Supplier<Map<String, ETagResponse>> mapBuilder = SoftHashMap::new; 064 065 final Map<String, ETagResponse> etagCache = Collections.synchronizedMap(mapBuilder.get()); 066 private final ThreadLocal<ETagResponse> currentETagRespThreadLocal = new ThreadLocal<>(); 067 private volatile boolean useCache = true; 068 069 @Override 070 protected void customizeConnection(HttpURLConnection connection) { 071 if (isUseCache() && connection.getRequestMethod().equals(HttpMethod.GET.name())) { 072 ETagResponse resp = etagCache.get(connection.getURL().toString()); 073 if (resp != null) { 074 currentETagRespThreadLocal.set(resp); 075 connection.addRequestProperty("If-None-Match", resp.getEtag()); 076 } 077 } 078 } 079 080 @Override 081 protected Response fetchResponse(HttpURLConnection httpUrlConnection) throws IOException { 082 try { 083 if (httpUrlConnection.getRequestMethod().equals(HttpMethod.GET.name())) { 084 if (httpUrlConnection.getResponseCode() == HTTP_NOT_MODIFIED && currentETagRespThreadLocal.get() != null) { 085 ETagResponse etagResp = currentETagRespThreadLocal.get(); 086 return new Response(httpUrlConnection.getResponseCode(), etagResp.getBody()); 087 } else { 088 Response resp = super.fetchResponse(httpUrlConnection); 089 if (httpUrlConnection.getHeaderField("ETag") != null) { 090 etagCache.put(httpUrlConnection.getURL().toString(), 091 new ETagResponse(httpUrlConnection.getHeaderField("ETag"), resp.getBody())); 092 } 093 return resp; 094 } 095 } else { 096 return super.fetchResponse(httpUrlConnection); 097 } 098 } finally { 099 currentETagRespThreadLocal.remove(); 100 } 101 } 102 103 /** 104 * return if cache is used. 105 * 106 * @return <code>true</code> if ETag-Cache is used, <code>false</code> if not 107 */ 108 public boolean isUseCache() { 109 return this.useCache; 110 } 111 112 /** 113 * activate/deactivate the ETag-Cache for the next request. 114 * 115 * <p> 116 * when deactivated, the ETag-Cache is *not* deleted 117 * </p> 118 * 119 * @param useCache 120 * flag to dis/enable the cache during runtime 121 */ 122 public void setUseCache(boolean useCache) { 123 this.useCache = useCache; 124 } 125 126 /** 127 * Override the mapSupplier, it needs to be some implementation of the {@link Map} interface. 128 * <p> 129 * You have to set this before the {@link ETagWebRequestor} object is created. While building it, the mapSupplier is 130 * used 131 * 132 * @param mapSupplier 133 * the supplier, that returns a new Map, 134 */ 135 public static void setMapSupplier(Supplier<Map<String, ETagResponse>> mapSupplier) { 136 ETagWebRequestor.mapBuilder = mapSupplier; 137 } 138 139 public static class ETagResponse { 140 141 public ETagResponse(String etag, String body) { 142 this.etag = etag; 143 this.body = body; 144 } 145 146 private final String etag; 147 private final String body; 148 149 public String getEtag() { 150 return etag; 151 } 152 153 public String getBody() { 154 return body; 155 } 156 } 157 158}