001/* 002 * Copyright (c) 2010-2026 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.io.InputStream; 028import java.net.http.HttpRequest; 029import java.net.http.HttpResponse; 030import java.util.Collections; 031import java.util.List; 032import java.util.Map; 033import java.util.function.Supplier; 034 035import com.restfb.util.SoftHashMap; 036 037/** 038 * WebRequestor with ETag-support. 039 * 040 * <p> 041 * The {@link ETagWebRequestor} caches all <tt>GET</tt>-requests with an ETag header field in a {@link SoftHashMap} and 042 * uses the ETag on the next request as <code>If-None-Match</code> header field if the same URL is requested. 043 * </p> 044 * 045 * <p> 046 * Is the response status code 304 (NOT MODIFIED) the old response from cache is used. 047 * </p> 048 * 049 * <p> 050 * <strong>Attention:</strong> even 304 responses count as request at Facebook and so they count against the throttling 051 * limits. Facebook suggests to use them for data that change only frequently 052 * </p> 053 * 054 * <p> 055 * Further information regarding ETag at Facebook can be found here: 056 * <a href="https://developers.facebook.com/blog/post/627/">https://developers.facebook.com/blog/post/627/</a> 057 * </p> 058 * 059 * <p> 060 * <strong>Attention 2</strong>: If excessively used with a lot of URLs, the {@link SoftHashMap} can lead to a 061 * performance degradation 062 * </p> 063 */ 064public class ETagWebRequestor extends DefaultWebRequestor { 065 066 private static Supplier<Map<String, ETagResponse>> mapBuilder = SoftHashMap::new; 067 068 final Map<String, ETagResponse> etagCache = Collections.synchronizedMap(mapBuilder.get()); 069 private final ThreadLocal<ETagResponse> currentETagRespThreadLocal = new ThreadLocal<>(); 070 private volatile boolean useCache = true; 071 072 @Override 073 protected void customizeRequest(HttpRequest.Builder builder, Request request, HttpMethod httpMethod) { 074 if (isUseCache() && HttpMethod.GET.equals(httpMethod)) { 075 ETagResponse resp = etagCache.get(request.getFullUrl()); 076 if (resp != null) { 077 currentETagRespThreadLocal.set(resp); 078 builder.header("If-None-Match", resp.getEtag()); 079 } 080 } 081 } 082 083 @Override 084 protected Response createResponse(HttpResponse<InputStream> httpResponse, Map<String, List<String>> headers) 085 throws IOException { 086 try { 087 if (HttpMethod.GET.name().equals(httpResponse.request().method())) { 088 if (httpResponse.statusCode() == HTTP_NOT_MODIFIED && currentETagRespThreadLocal.get() != null) { 089 closeQuietly(httpResponse.body()); 090 ETagResponse etagResp = currentETagRespThreadLocal.get(); 091 return new Response(httpResponse.statusCode(), etagResp.getBody(), null, headers); 092 } else { 093 Response resp = super.createResponse(httpResponse, headers); 094 String fullUrl = httpResponse.request().uri().toString(); 095 httpResponse.headers().firstValue("ETag") 096 .ifPresent(etag -> etagCache.put(fullUrl, new ETagResponse(etag, resp.getBody()))); 097 return resp; 098 } 099 } else { 100 return super.createResponse(httpResponse, headers); 101 } 102 } finally { 103 currentETagRespThreadLocal.remove(); 104 } 105 } 106 107 /** 108 * return if cache is used. 109 * 110 * @return <code>true</code> if ETag-Cache is used, <code>false</code> if not 111 */ 112 public boolean isUseCache() { 113 return this.useCache; 114 } 115 116 /** 117 * activate/deactivate the ETag-Cache for the next request. 118 * 119 * <p> 120 * when deactivated, the ETag-Cache is *not* deleted 121 * </p> 122 * 123 * @param useCache 124 * flag to dis/enable the cache during runtime 125 */ 126 public void setUseCache(boolean useCache) { 127 this.useCache = useCache; 128 } 129 130 /** 131 * Override the mapSupplier, it needs to be some implementation of the {@link Map} interface. 132 * <p> 133 * You have to set this before the {@link ETagWebRequestor} object is created. While building it, the mapSupplier is 134 * used 135 * 136 * @param mapSupplier 137 * the supplier, that returns a new Map, 138 */ 139 public static void setMapSupplier(Supplier<Map<String, ETagResponse>> mapSupplier) { 140 ETagWebRequestor.mapBuilder = mapSupplier; 141 } 142 143 public static class ETagResponse { 144 145 public ETagResponse(String etag, String body) { 146 this.etag = etag; 147 this.body = body; 148 } 149 150 private final String etag; 151 private final String body; 152 153 public String getEtag() { 154 return etag; 155 } 156 157 public String getBody() { 158 return body; 159 } 160 } 161 162}