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}