/* * Copyright 2006-2008 Sxip Identity Corporation */ package org.openid4java.util; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.HeadMethod; import org.apache.commons.httpclient.cookie.CookiePolicy; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.util.Map; import java.util.Iterator; import java.util.HashMap; import java.io.InputStream; import java.io.IOException; /** * Wrapper cache around HttpClient providing caching for HTTP requests. * Intended to be used to optimize the number of HTTP requests performed * during OpenID discovery. * * @author Marius Scurtescu, Johnny Bufu */ public class HttpCache { private static Log _log = LogFactory.getLog(HttpCache.class); private static final boolean DEBUG = _log.isDebugEnabled(); /** * HttpClient used to place the HTTP requests. */ private HttpClient _client; /** * Default set of HTTP request options to be used when placing HTTP * requests, if a custom one was not specified. */ private HttpRequestOptions _defaultOptions = new HttpRequestOptions(); /** * Cache for GET requests. Map of URL -> HttpResponse. */ private Map _getCache = new HashMap(); // todo: cache management /** * Cache for HEAD requests. Map of URL -> HttpResponse. */ private Map _headCache = new HashMap(); /** * Constructs a new HttpCache object, that will be initialized with the * default set of HttpRequestOptions. * * @see HttpRequestOptions */ public HttpCache() { _client = HttpClientFactory.getInstance( _defaultOptions.getMaxRedirects(), Boolean.TRUE, _defaultOptions.getSocketTimeout(), _defaultOptions.getConnTimeout(), CookiePolicy.IGNORE_COOKIES); } public HttpRequestOptions getDefaultRequestOptions() { return _defaultOptions; } /** * Gets a clone of the default HttpRequestOptions. * @return */ public HttpRequestOptions getRequestOptions() { return new HttpRequestOptions(_defaultOptions); } public void setDefaultRequestOptions(HttpRequestOptions defaultOptions) { this._defaultOptions = defaultOptions; } /** * Removes a cached GET response. * * @param url The URL for which to remove the cached response. */ public void removeGet(String url) { if (_getCache.keySet().contains(url)) { _log.info("Removing cached GET response for " + url); _getCache.remove(url); } else _log.info("NOT removing cached GET for " + url + " NOT FOUND."); } /** * GETs a HTTP URL. A cached copy will be returned if one exists. * * @param url The HTTP URL to GET. * @return A HttpResponse object containing the fetched data. * * @see HttpResponse */ public HttpResponse get(String url) throws IOException { return get(url, _defaultOptions); } /** * GETs a HTTP URL. A cached copy will be returned if one exists and the * supplied options match it. * * @param url The HTTP URL to GET. * @return A HttpResponse object containing the fetched data. * * @see HttpRequestOptions, HttpResponse */ public HttpResponse get(String url, HttpRequestOptions requestOptions) throws IOException { HttpResponse resp = (HttpResponse) _getCache.get(url); if (resp != null) { if (match(resp, requestOptions)) { _log.info("Returning cached GET response for " + url); return resp; } else { _log.info("Removing cached GET for " + url); removeGet(url); } } GetMethod get = new GetMethod(url); try { get.setFollowRedirects(true); _client.getParams().setParameter( "http.protocol.max-redirects", new Integer(requestOptions.getMaxRedirects())); _client.getParams().setSoTimeout(requestOptions.getSocketTimeout()); _client.getHttpConnectionManager().getParams().setConnectionTimeout( requestOptions.getConnTimeout()); Map requestHeaders = requestOptions.getRequestHeaders(); if (requestHeaders != null) { Iterator iter = requestHeaders.keySet().iterator(); String headerName; while (iter.hasNext()) { headerName = (String) iter.next(); get.setRequestHeader(headerName, (String) requestHeaders.get(headerName)); } } int statusCode = _client.executeMethod(get); String statusLine = get.getStatusLine().toString(); String httpBody = null; boolean bodySizeExceeded = false; int maxBodySize = requestOptions.getMaxBodySize(); InputStream httpBodyInput = get.getResponseBodyAsStream(); if (httpBodyInput != null) { byte data[] = new byte[maxBodySize]; int totalRead = 0; int currentRead; while (totalRead < maxBodySize) { currentRead = httpBodyInput.read( data, totalRead, maxBodySize - totalRead); if (currentRead == -1) break; totalRead += currentRead; } if (httpBodyInput.read() > 0) bodySizeExceeded = true; httpBodyInput.close(); if (DEBUG) _log.debug("Read " + totalRead + " bytes."); httpBody = new String(data, 0, totalRead); } resp = new HttpResponse(statusCode, statusLine, requestOptions.getMaxRedirects(), get.getURI().toString(), get.getResponseHeaders(), httpBody); resp.setBodySizeExceeded(bodySizeExceeded); // save result in cache _getCache.put(url, resp); } finally { get.releaseConnection(); } return resp; } private boolean match(HttpResponse resp, HttpRequestOptions requestOptions) { // use cache? if ( resp != null && ! requestOptions.isUseCache()) { _log.info("Explicit fresh GET requested; removing cached copy"); return false; } // content type rules String requiredContentType = requestOptions.getContentType(); if (resp != null && requiredContentType != null) { Header responseContentType = resp.getResponseHeader("content-type"); if ( responseContentType != null && responseContentType.getValue() != null && !responseContentType.getValue().split(";")[0] .equalsIgnoreCase(requiredContentType) ) { _log.info("Cached GET response does not match " + "the required content type, removing."); return false; } } if (resp != null && resp.getMaxRedirectsFollowed() > requestOptions.getMaxRedirects()) { _log.info("Cached GET response used " + resp.getMaxRedirectsFollowed() + " max redirects; current requirement is: " + requestOptions.getMaxRedirects()); return false; } return true; } public HttpResponse head(String url) throws IOException { return head(url, _defaultOptions); } public HttpResponse head(String url, HttpRequestOptions requestOptions) throws IOException { HttpResponse resp = (HttpResponse) _headCache.get(url); if (resp != null) { if (match(resp, requestOptions)) { _log.info("Returning cached HEAD response for " + url); return resp; } else { _log.info("Removing cached HEAD for " + url); removeGet(url); } } HeadMethod head = new HeadMethod(url); try { head.setFollowRedirects(true); _client.getParams().setParameter( "http.protocol.max-redirects", new Integer(requestOptions.getMaxRedirects())); _client.getParams().setSoTimeout(requestOptions.getSocketTimeout()); _client.getHttpConnectionManager().getParams().setConnectionTimeout( requestOptions.getConnTimeout()); int statusCode = _client.executeMethod(head); String statusLine = head.getStatusLine().toString(); resp = new HttpResponse(statusCode, statusLine, requestOptions.getMaxRedirects(), head.getURI().toString(), head.getResponseHeaders(), null); // save result in cache _headCache.put(url, resp); } finally { head.releaseConnection(); } return resp; } }