Repository: cxf Updated Branches: refs/heads/master 9283d10ad -> 2200660aa
[CXF-5996] Initial support for client-side caching, patch from Romain Manni-Bucau applied Project: http://git-wip-us.apache.org/repos/asf/cxf/repo Commit: http://git-wip-us.apache.org/repos/asf/cxf/commit/2200660a Tree: http://git-wip-us.apache.org/repos/asf/cxf/tree/2200660a Diff: http://git-wip-us.apache.org/repos/asf/cxf/diff/2200660a Branch: refs/heads/master Commit: 2200660aa3ec40942b50161abee6ebf776750ed3 Parents: 9283d10 Author: Sergey Beryozkin <sberyoz...@talend.com> Authored: Tue Nov 25 21:35:18 2014 +0000 Committer: Sergey Beryozkin <sberyoz...@talend.com> Committed: Tue Nov 25 21:44:58 2014 +0000 ---------------------------------------------------------------------- rt/rs/client/pom.xml | 19 +++ .../CacheControlClientReaderInterceptor.java | 118 ++++++++++++++++++ .../cache/CacheControlClientRequestFilter.java | 78 ++++++++++++ .../jaxrs/client/cache/CacheControlFeature.java | 123 +++++++++++++++++++ .../cxf/jaxrs/client/cache/ClientCache.java | 32 +++++ .../apache/cxf/jaxrs/client/cache/Entry.java | 94 ++++++++++++++ .../org/apache/cxf/jaxrs/client/cache/Key.java | 79 ++++++++++++ .../cxf/jaxrs/client/cache/ClientCacheTest.java | 98 +++++++++++++++ 8 files changed, 641 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cxf/blob/2200660a/rt/rs/client/pom.xml ---------------------------------------------------------------------- diff --git a/rt/rs/client/pom.xml b/rt/rs/client/pom.xml index ee50a8f..b503b25 100644 --- a/rt/rs/client/pom.xml +++ b/rt/rs/client/pom.xml @@ -78,6 +78,13 @@ <version>${project.version}</version> </dependency> <dependency> + <groupId>org.apache.geronimo.specs</groupId> + <artifactId>geronimo-jcache_1.0_spec</artifactId> + <version>1.0-alpha-1</version> + <scope>provided</scope> + <optional>true</optional> + </dependency> + <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <scope>provided</scope> @@ -104,5 +111,17 @@ <artifactId>slf4j-jdk14</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-jcs-jcache</artifactId> + <version>2.0-SNAPSHOT</version> + <scope>test</scope> + </dependency> </dependencies> + <repositories> + <repository> + <id>jboss</id> + <url>http://repository.jboss.org/nexus/content/groups/public/</url> + </repository> + </repositories> </project> http://git-wip-us.apache.org/repos/asf/cxf/blob/2200660a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/CacheControlClientReaderInterceptor.java ---------------------------------------------------------------------- diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/CacheControlClientReaderInterceptor.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/CacheControlClientReaderInterceptor.java new file mode 100644 index 0000000..8c9e1f6 --- /dev/null +++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/CacheControlClientReaderInterceptor.java @@ -0,0 +1,118 @@ +/** + * 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.IOException; +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.transport.http.Headers; + +@ClientCache +@Priority(Priorities.USER - 1) +public class CacheControlClientReaderInterceptor implements ReaderInterceptor { + private Cache<Key, Entry> cache; + + @Context + private UriInfo uriInfo; + + public CacheControlClientReaderInterceptor(final Cache<Key, Entry> cache) { + setCache(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 { + if (Boolean.parseBoolean((String)context.getProperty("no_client_cache"))) { + return context.proceed(); + } + final MultivaluedMap<String, String> headers = context.getHeaders(); + final String cacheControlHeader = headers.getFirst(HttpHeaders.CACHE_CONTROL); + String expiresHeader = headers.getFirst(HttpHeaders.EXPIRES); + + long expiry = -1; + if (cacheControlHeader != null) { + final CacheControl value = CacheControl.valueOf(cacheControlHeader.toString()); + if (value.isNoCache()) { + return context.proceed(); + } + expiry = value.getMaxAge(); + } else if (expiresHeader != null) { + 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) { + // try next + } + + } else { // no cache + return context.proceed(); + } + + final Object proceed = context.proceed(); + + final Entry entry = new Entry(((String)proceed).getBytes(), context.getHeaders(), + computeCacheHeaders(context.getHeaders()), expiry); + final URI uri = uriInfo.getRequestUri(); + final String accepts = headers.getFirst(HttpHeaders.ACCEPT); + cache.put(new Key(uri, accepts), entry); + + return proceed; + } + + private Map<String, String> computeCacheHeaders(final MultivaluedMap<String, String> httpHeaders) { + final Map<String, String> cacheHeaders = new HashMap<String, String>(2); + + final String etagHeader = httpHeaders.getFirst(HttpHeaders.ETAG); + if (etagHeader != null) { + cacheHeaders.put("If-None-Match", etagHeader); + } + final String lastModifiedHeader = httpHeaders.getFirst(HttpHeaders.LAST_MODIFIED); + if (lastModifiedHeader != null) { + cacheHeaders.put("If-Modified-Since", lastModifiedHeader); + } + + return cacheHeaders; + } +} http://git-wip-us.apache.org/repos/asf/cxf/blob/2200660a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/CacheControlClientRequestFilter.java ---------------------------------------------------------------------- diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/CacheControlClientRequestFilter.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/CacheControlClientRequestFilter.java new file mode 100644 index 0000000..3656c9f --- /dev/null +++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/CacheControlClientRequestFilter.java @@ -0,0 +1,78 @@ +/** + * 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.IOException; +import java.net.URI; +import java.util.List; +import java.util.Map; + +import javax.annotation.Priority; +import javax.cache.Cache; +import javax.ws.rs.Priorities; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +@ClientCache +@Priority(Priorities.USER - 1) +public class CacheControlClientRequestFilter implements ClientRequestFilter { + private Cache<Key, Entry> cache; + + public CacheControlClientRequestFilter(final Cache<Key, Entry> cache) { + setCache(cache); + } + + public CacheControlClientRequestFilter() { + // no-op: use setCache then + } + + @Override + public void filter(final ClientRequestContext request) throws IOException { + if (!"GET".equals(request.getMethod())) { + request.setProperty("no_client_cache", "true"); + return; + } + final URI uri = request.getUri(); + final String accepts = request.getHeaderString(HttpHeaders.ACCEPT); + final Key key = new Key(uri, accepts); + Entry entry = cache.get(key); + if (entry != null) { + if (entry.isOutDated()) { + cache.remove(key, entry); + } else { + Response.ResponseBuilder ok = Response.ok(new String(entry.getData())); + if (entry.getHeaders() != null) { + for (Map.Entry<String, List<String>> h : entry.getHeaders().entrySet()) { + for (final Object instance : h.getValue()) { + ok = ok.header(h.getKey(), instance); + } + } + } + request.abortWith(ok.build()); + } + } + } + + public CacheControlClientRequestFilter setCache(final Cache<Key, Entry> c) { + this.cache = c; + return this; + } +} http://git-wip-us.apache.org/repos/asf/cxf/blob/2200660a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/CacheControlFeature.java ---------------------------------------------------------------------- diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/CacheControlFeature.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/CacheControlFeature.java new file mode 100644 index 0000000..9b626d5 --- /dev/null +++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/CacheControlFeature.java @@ -0,0 +1,123 @@ +/** + * 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.Closeable; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Map; +import java.util.Properties; + +import javax.annotation.PreDestroy; +import javax.annotation.Priority; +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.Caching; +import javax.cache.configuration.Factory; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.spi.CachingProvider; +import javax.ws.rs.Priorities; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; + + + +@Priority(Priorities.HEADER_DECORATOR) +public class CacheControlFeature implements Feature { + private CachingProvider provider; + private CacheManager manager; + private Cache<Key, Entry> cache; + + @Override + public boolean configure(final FeatureContext context) { + // TODO: read context properties to exclude some patterns? + final Cache<Key, Entry> entryCache = createCache(context.getConfiguration().getProperties()); + context.register(new CacheControlClientRequestFilter(entryCache)); + context.register(new CacheControlClientReaderInterceptor(entryCache)); + return true; + } + + @PreDestroy // TODO: check it is called + public void close() { + for (final Closeable c : Arrays.asList(cache, manager, provider)) { + try { + if (c != null) { + c.close(); + } + } catch (final Exception e) { + // no-op + } + } + } + + private Cache<Key, Entry> createCache(final Map<String, Object> properties) { + final Properties props = new Properties(); + props.putAll(properties); + + final String prefix = ClientCache.class.getName() + "."; + final String uri = props.getProperty(prefix + "config-uri"); + final String name = props.getProperty(prefix + "name", ClientCache.class.getName()); + + final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + + provider = Caching.getCachingProvider(); + try { + manager = provider.getCacheManager( + uri == null ? provider.getDefaultURI() : new URI(uri), + contextClassLoader, + props); + + final MutableConfiguration<Key, Entry> configuration = new MutableConfiguration<Key, Entry>() + .setReadThrough("true".equalsIgnoreCase(props.getProperty(prefix + "readThrough", "false"))) + .setWriteThrough("true".equalsIgnoreCase(props.getProperty(prefix + "writeThrough", "false"))) + .setManagementEnabled( + "true".equalsIgnoreCase(props.getProperty(prefix + "managementEnabled", "false"))) + .setStatisticsEnabled( + "true".equalsIgnoreCase(props.getProperty(prefix + "statisticsEnabled", "false"))) + .setStoreByValue("true".equalsIgnoreCase(props.getProperty(prefix + "storeByValue", "false"))); + + final String loader = props.getProperty(prefix + "loaderFactory"); + if (loader != null) { + configuration.setCacheLoaderFactory(newInstance(contextClassLoader, loader, Factory.class)); + } + final String writer = props.getProperty(prefix + "writerFactory"); + if (writer != null) { + configuration.setCacheWriterFactory(newInstance(contextClassLoader, writer, Factory.class)); + } + final String expiry = props.getProperty(prefix + "expiryFactory"); + if (expiry != null) { + configuration.setExpiryPolicyFactory(newInstance(contextClassLoader, expiry, Factory.class)); + } + + cache = manager.createCache(name, configuration); + return cache; + } catch (final URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + private static <T> T newInstance(final ClassLoader contextClassLoader, final String clazz, final Class<T> cast) { + try { + return (T) contextClassLoader.loadClass(clazz).newInstance(); + } catch (final Exception e) { + throw new IllegalArgumentException(e); + } + } +} http://git-wip-us.apache.org/repos/asf/cxf/blob/2200660a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/ClientCache.java ---------------------------------------------------------------------- diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/ClientCache.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/ClientCache.java new file mode 100644 index 0000000..45f629a --- /dev/null +++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/ClientCache.java @@ -0,0 +1,32 @@ +/** + * 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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.ws.rs.NameBinding; + +@NameBinding +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface ClientCache { +} http://git-wip-us.apache.org/repos/asf/cxf/blob/2200660a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/Entry.java ---------------------------------------------------------------------- diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/Entry.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/Entry.java new file mode 100644 index 0000000..4f01713 --- /dev/null +++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/Entry.java @@ -0,0 +1,94 @@ +/** + * 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.Serializable; +import java.util.Collections; +import java.util.Map; + +import javax.ws.rs.core.MultivaluedMap; + +public class Entry implements Serializable { + private static final long serialVersionUID = -3551501551331222546L; + private Map<String, String> cacheHeaders = Collections.emptyMap(); + private byte[] data; + private MultivaluedMap<String, String> headers; + private long expiresValue; + private long initialTimestamp = now(); + + public Entry(final byte[] bytes, final MultivaluedMap<String, String> headers, + final Map<String, String> cacheHeaders, final long expiresHeaderValue) { + this.data = bytes; + this.headers = headers; + this.cacheHeaders = cacheHeaders; + this.expiresValue = expiresHeaderValue; + } + + public Entry() { + // no-op + } + + public boolean isOutDated() { + return now() - initialTimestamp > expiresValue * 1000; + } + + public Map<String, String> getCacheHeaders() { + return cacheHeaders; + } + + public void setCacheHeaders(final Map<String, String> cacheHeaders) { + this.cacheHeaders = cacheHeaders; + } + + public byte[] getData() { + return data; + } + + public void setData(final byte[] data) { + this.data = data; + } + + public MultivaluedMap<String, String> getHeaders() { + return headers; + } + + public void setHeaders(final MultivaluedMap<String, String> headers) { + this.headers = headers; + } + + public long getExpiresValue() { + return expiresValue; + } + + public void setExpiresValue(final long expiresValue) { + this.expiresValue = expiresValue; + } + + public long getInitialTimestamp() { + return initialTimestamp; + } + + public void setInitialTimestamp(final long initialTimestamp) { + this.initialTimestamp = initialTimestamp; + } + + private static long now() { + return System.currentTimeMillis(); + } +} http://git-wip-us.apache.org/repos/asf/cxf/blob/2200660a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/Key.java ---------------------------------------------------------------------- diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/Key.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/Key.java new file mode 100644 index 0000000..f794a82 --- /dev/null +++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/cache/Key.java @@ -0,0 +1,79 @@ +/** + * 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.Serializable; +import java.net.URI; + +public class Key implements Serializable { + private static final long serialVersionUID = 400974121100289840L; + + private int hash; + + private URI uri; + private String accept; + + public Key(final URI uri, final String accept) { + this.uri = uri; + this.accept = accept; + + int result = uri.hashCode(); + result = 31 * result + (accept != null ? accept.hashCode() : 0); + this.hash = result; + } + + public Key() { + // no-op + } + + public URI getUri() { + return uri; + } + + public void setUri(final URI uri) { + this.uri = uri; + } + + public String getAccept() { + return accept; + } + + public void setAccept(final String accept) { + this.accept = accept; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || Key.class != o.getClass()) { + return false; + } + + final Key key = Key.class.cast(o); + return !(accept != null ? !accept.equals(key.accept) : key.accept != null) && uri.equals(key.uri); + + } + + @Override + public int hashCode() { + return hash; + } +} http://git-wip-us.apache.org/repos/asf/cxf/blob/2200660a/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/cache/ClientCacheTest.java ---------------------------------------------------------------------- diff --git a/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/cache/ClientCacheTest.java b/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/cache/ClientCacheTest.java new file mode 100644 index 0000000..8b141f0 --- /dev/null +++ b/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/cache/ClientCacheTest.java @@ -0,0 +1,98 @@ +/** + * 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.net.HttpURLConnection; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import org.apache.cxf.endpoint.Server; +import org.apache.cxf.jaxrs.JAXRSServerFactoryBean; +import org.apache.cxf.jaxrs.client.WebClient; +import org.apache.cxf.jaxrs.client.spec.InvocationBuilderImpl; +import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider; +import org.apache.cxf.transport.local.LocalConduit; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +public class ClientCacheTest extends Assert { + public static final String ADDRESS = "local://transport"; + private static Server server; + + @BeforeClass + public static void bind() throws Exception { + final JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean(); + sf.setResourceClasses(TheServer.class); + sf.setResourceProvider(TheServer.class, new SingletonResourceProvider(new TheServer(), false)); + sf.setAddress(ADDRESS); + server = sf.create(); + } + + @AfterClass + public static void unbind() throws Exception { + server.stop(); + server.destroy(); + } + + @Test + public void testCache() { + final WebTarget base = ClientBuilder.newBuilder().register(CacheControlFeature.class).build().target(ADDRESS); + final Invocation.Builder cached = setAsLocal(base.request()).header(HttpHeaders.CACHE_CONTROL, "public"); + final Response r = cached.get(); + assertEquals(r.getStatus(), HttpURLConnection.HTTP_OK); + final String r1 = r.readEntity(String.class); + waitABit(); + assertEquals(r1, cached.get().readEntity(String.class)); + } + + private static Invocation.Builder setAsLocal(final Invocation.Builder client) { + WebClient.getConfig(InvocationBuilderImpl.class.cast(client).getWebClient()) + .getRequestContext().put(LocalConduit.DIRECT_DISPATCH, Boolean.TRUE); + return client; + } + + private static void waitABit() { + try { // just to be sure + Thread.sleep(150); + } catch (final InterruptedException e) { + Thread.interrupted(); + } + } + + @Path("/") + public static class TheServer { + @GET + @ClientCache + public Response array() { + return Response.ok(Long.toString(System.currentTimeMillis())) + .tag("123").cacheControl(CacheControl.valueOf("max-age=50000")).build(); + } + } +}