001/*
002 * Copyright (c) 2010-2021 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   * 
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  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}