/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.cxf.jaxrs.client.cache; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.net.URI; import java.text.ParseException; import java.util.HashMap; import java.util.Map; import javax.annotation.Priority; import javax.cache.Cache; import javax.ws.rs.Priorities; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.ReaderInterceptor; import javax.ws.rs.ext.ReaderInterceptorContext; import org.apache.cxf.helpers.IOUtils; import org.apache.cxf.transport.http.Headers; @Priority(Priorities.USER - 1) public class CacheControlClientReaderInterceptor implements ReaderInterceptor { private Cache<Key, Entry> cache; @Context private UriInfo uriInfo; private boolean cacheResponseInputStream; public CacheControlClientReaderInterceptor(final Cache<Key, Entry> cache) { this.cache = cache; } public CacheControlClientReaderInterceptor() { // no-op: use setCache then } public CacheControlClientReaderInterceptor setCache(final Cache<Key, Entry> c) { this.cache = c; return this; } @Override public Object aroundReadFrom(final ReaderInterceptorContext context) throws IOException, WebApplicationException { Object cachedEntity = context.getProperty(CacheControlClientRequestFilter.CACHED_ENTITY_PROPERTY); if (cachedEntity != null) { if (cachedEntity instanceof BytesEntity) { // InputStream or byte[] BytesEntity bytesEntity = (BytesEntity)cachedEntity; byte[] bytes = bytesEntity.getEntity(); cachedEntity = bytesEntity.isFromStream() ? new ByteArrayInputStream(bytes) : bytes; if (cacheResponseInputStream) { InputStream is = bytesEntity.isFromStream() ? (InputStream)cachedEntity : new ByteArrayInputStream((byte[])cachedEntity); context.setInputStream(is); return context.proceed(); } } return cachedEntity; } if (Boolean.parseBoolean((String)context.getProperty(CacheControlClientRequestFilter.NO_CACHE_PROPERTY))) { // non GET HTTP method or other restriction applies return context.proceed(); } final MultivaluedMap<String, String> responseHeaders = context.getHeaders(); final String cacheControlHeader = responseHeaders.getFirst(HttpHeaders.CACHE_CONTROL); final CacheControl cacheControl = CacheControl.valueOf(cacheControlHeader.toString()); byte[] cachedBytes = null; final boolean validCacheControl = isCacheControlValid(context, cacheControl); if (validCacheControl && cacheResponseInputStream) { // if Cache-Control is set and the stream needs to be cached then do it cachedBytes = IOUtils.readBytesFromStream((InputStream)context.getInputStream()); context.setInputStream(new ByteArrayInputStream(cachedBytes)); } // Read the stream and get the actual entity Object responseEntity = context.proceed(); if (!validCacheControl) { return responseEntity; } // if a max-age property is set then it overrides Expires long expiry = cacheControl.getMaxAge(); if (expiry == -1) { //TODO: Review if Expires can be supported as an alternative to Cache-Control String expiresHeader = responseHeaders.getFirst(HttpHeaders.EXPIRES); if (expiresHeader.startsWith("'") && expiresHeader.endsWith("'")) { expiresHeader = expiresHeader.substring(1, expiresHeader.length() - 1); } try { expiry = (Headers.getHttpDateFormat().parse(expiresHeader).getTime() - System.currentTimeMillis()) / 1000; } catch (final ParseException e) { // TODO: Revisit the possibility of supporting multiple formats } } Serializable ser = null; if (cachedBytes != null) { // store the cached bytes - they will be parsed again when a client cache will return them ser = new BytesEntity(cachedBytes, responseEntity instanceof InputStream); } else if (responseEntity instanceof Serializable) { // store the entity directly ser = (Serializable)responseEntity; } else if (responseEntity instanceof InputStream) { // read the stream, cache it, the cached bytes will be returned immediately // when a client cache will return them byte[] bytes = IOUtils.readBytesFromStream((InputStream)responseEntity); ser = new BytesEntity(bytes, true); responseEntity = new ByteArrayInputStream(bytes); } else if (responseEntity instanceof byte[]) { // the cached bytes will be returned immediately when a client cache will return them ser = new BytesEntity((byte[])responseEntity, false); } if (ser != null) { final Entry entry = new Entry(ser, responseHeaders, computeCacheHeaders(responseHeaders), expiry); final URI uri = uriInfo.getRequestUri(); final String accepts = (String)context.getProperty(CacheControlClientRequestFilter.CLIENT_ACCEPTS); cache.put(new Key(uri, accepts), entry); } return responseEntity; } private Map<String, String> computeCacheHeaders(final MultivaluedMap<String, String> responseHeaders) { final Map<String, String> cacheHeaders = new HashMap<>(2); final String etagHeader = responseHeaders.getFirst(HttpHeaders.ETAG); if (etagHeader != null) { cacheHeaders.put(HttpHeaders.IF_NONE_MATCH, etagHeader); } final String lastModifiedHeader = responseHeaders.getFirst(HttpHeaders.LAST_MODIFIED); if (lastModifiedHeader != null) { cacheHeaders.put(HttpHeaders.IF_MODIFIED_SINCE, lastModifiedHeader); } return cacheHeaders; } public boolean isCacheInputStream() { return cacheResponseInputStream; } /** * Enforce the caching of the response stream. * This is not recommended if the client code expects Serializable data, * example, String or custom JAXB beans marked as Serializable, * which can be stored in the cache directly. * Use this property only if returning a cached entity does require a * repeated stream parsing. * * @param cacheInputStream */ public void setCacheResponseInputStream(boolean cacheInputStream) { this.cacheResponseInputStream = cacheInputStream; } protected boolean isCacheControlValid(final ReaderInterceptorContext context, final CacheControl responseControl) { boolean valid = responseControl != null && !responseControl.isNoCache() && !responseControl.isNoStore(); if (valid) { String clientHeader = (String)context.getProperty(CacheControlClientRequestFilter.CLIENT_CACHE_CONTROL); CacheControl clientControl = clientHeader == null ? null : CacheControl.valueOf(clientHeader); if (clientControl != null && clientControl.isPrivate() != responseControl.isPrivate()) { valid = false; } } return valid; } }