From a16231657fe29334a589c98290ac8b6b2710a144 Mon Sep 17 00:00:00 2001 From: "Benjamin, Max" Date: Thu, 19 Nov 2020 18:12:50 -0500 Subject: add caching to graph inventory client add caching to graph inventory client updated properties files to read cache properties Issue-ID: SO-3398 Signed-off-by: Benjamin, Max (mb388a) Change-Id: Ib3e67ae014b6668c9b004aae1e8b5d49b9ce6b06 --- .../onap/aaiclient/client/CacheControlFeature.java | 137 +++++++++++++++++++++ .../org/onap/aaiclient/client/CacheLogger.java | 53 ++++++++ .../java/org/onap/aaiclient/client/FlushCache.java | 41 ++++++ .../onap/aaiclient/client/aai/AAIProperties.java | 12 ++ .../graphinventory/GraphInventoryRestClient.java | 20 +++ .../aaiclient/client/aai/AAIRestClientTest.java | 116 +++++++++++++++++ 6 files changed, 379 insertions(+) create mode 100644 graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/CacheControlFeature.java create mode 100644 graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/CacheLogger.java create mode 100644 graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/FlushCache.java (limited to 'graph-inventory/aai-client') diff --git a/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/CacheControlFeature.java b/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/CacheControlFeature.java new file mode 100644 index 0000000000..1e7c3e7f71 --- /dev/null +++ b/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/CacheControlFeature.java @@ -0,0 +1,137 @@ +package org.onap.aaiclient.client; + +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.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.Caching; +import javax.cache.configuration.Factory; +import javax.cache.configuration.FactoryBuilder; +import javax.cache.configuration.MutableCacheEntryListenerConfiguration; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.expiry.ExpiryPolicy; +import javax.cache.integration.CacheLoader; +import javax.cache.integration.CacheWriter; +import javax.cache.spi.CachingProvider; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.ext.Provider; +import org.apache.cxf.jaxrs.client.cache.CacheControlClientReaderInterceptor; +import org.apache.cxf.jaxrs.client.cache.CacheControlClientRequestFilter; +import org.apache.cxf.jaxrs.client.cache.Entry; +import org.apache.cxf.jaxrs.client.cache.Key; + + +@Provider +public class CacheControlFeature implements Feature, Closeable { + private CachingProvider provider; + private CacheManager manager; + private Cache cache; + private boolean cacheResponseInputStream; + private Factory expiryPolicy; + + @Override + public boolean configure(final FeatureContext context) { + // TODO: read context properties to exclude some patterns? + final Cache entryCache = createCache(context.getConfiguration().getProperties()); + context.register(new CacheControlClientRequestFilter(entryCache)); + CacheControlClientReaderInterceptor reader = new CacheControlClientReaderInterceptor(entryCache); + reader.setCacheResponseInputStream(cacheResponseInputStream); + context.register(reader); + 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 createCache(final Map properties) { + final Properties props = new Properties(); + props.putAll(properties); + + final String prefix = this.getClass().getName() + "."; + final String uri = props.getProperty(prefix + "config-uri"); + final String name = props.getProperty(prefix + "name", this.getClass().getName()); + + final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + + provider = Caching.getCachingProvider(); + try { + synchronized (contextClassLoader) { + manager = provider.getCacheManager(uri == null ? provider.getDefaultURI() : new URI(uri), + contextClassLoader, props); + if (manager.getCache(name) == null) { + final MutableConfiguration configuration = new MutableConfiguration() + .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) { + @SuppressWarnings("unchecked") + Factory> f = + newInstance(contextClassLoader, loader, Factory.class); + configuration.setCacheLoaderFactory(f); + } + final String writer = props.getProperty(prefix + "writerFactory"); + if (writer != null) { + @SuppressWarnings("unchecked") + Factory> f = + newInstance(contextClassLoader, writer, Factory.class); + configuration.setCacheWriterFactory(f); + } + if (expiryPolicy != null) { + + configuration.setExpiryPolicyFactory(expiryPolicy); + } + configuration.addCacheEntryListenerConfiguration(new MutableCacheEntryListenerConfiguration( + FactoryBuilder.factoryOf(new CacheLogger()), null, true, true)); + + cache = manager.createCache(name, configuration); + } else { + cache = manager.getCache(name); + } + return cache; + } + } catch (final URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + @SuppressWarnings("unchecked") + private static T newInstance(final ClassLoader contextClassLoader, final String clazz, final Class cast) { + try { + return (T) contextClassLoader.loadClass(clazz).newInstance(); + } catch (final Exception e) { + throw new IllegalArgumentException(e); + } + } + + public void setCacheResponseInputStream(boolean cacheStream) { + this.cacheResponseInputStream = cacheStream; + } + + public void setExpiryPolicyFactory(Factory f) { + this.expiryPolicy = f; + } +} diff --git a/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/CacheLogger.java b/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/CacheLogger.java new file mode 100644 index 0000000000..f3dc610125 --- /dev/null +++ b/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/CacheLogger.java @@ -0,0 +1,53 @@ +package org.onap.aaiclient.client; + +import java.io.Serializable; +import javax.cache.event.CacheEntryCreatedListener; +import javax.cache.event.CacheEntryEvent; +import javax.cache.event.CacheEntryExpiredListener; +import javax.cache.event.CacheEntryListenerException; +import javax.cache.event.CacheEntryRemovedListener; +import javax.cache.event.CacheEntryUpdatedListener; +import org.apache.cxf.jaxrs.client.cache.Entry; +import org.apache.cxf.jaxrs.client.cache.Key; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CacheLogger implements CacheEntryExpiredListener, CacheEntryCreatedListener, + CacheEntryUpdatedListener, CacheEntryRemovedListener, Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger logger = LoggerFactory.getLogger(CacheLogger.class); + + @Override + public void onExpired(Iterable> events) + throws CacheEntryListenerException { + for (CacheEntryEvent event : events) { + logger.debug("{} expired key: {}", event.getSource().getName(), event.getKey().getUri()); + } + } + + @Override + public void onRemoved(Iterable> events) + throws CacheEntryListenerException { + for (CacheEntryEvent event : events) { + logger.debug("{} removed key: {}", event.getSource().getName(), event.getKey().getUri()); + } + } + + @Override + public void onUpdated(Iterable> events) + throws CacheEntryListenerException { + for (CacheEntryEvent event : events) { + logger.debug("{} updated key: {}", event.getSource().getName(), event.getKey().getUri()); + } + } + + @Override + public void onCreated(Iterable> events) + throws CacheEntryListenerException { + for (CacheEntryEvent event : events) { + logger.debug("{} created key: {}", event.getSource().getName(), event.getKey().getUri()); + } + } + +} diff --git a/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/FlushCache.java b/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/FlushCache.java new file mode 100644 index 0000000000..0f290ff620 --- /dev/null +++ b/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/FlushCache.java @@ -0,0 +1,41 @@ +package org.onap.aaiclient.client; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import javax.cache.CacheManager; +import javax.cache.Caching; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import org.apache.cxf.jaxrs.client.cache.Key; +import org.onap.so.client.CacheProperties; + +public class FlushCache implements ClientResponseFilter { + + private static final Set modifyMethods = + new HashSet<>(Arrays.asList(HttpMethod.DELETE, HttpMethod.PATCH, HttpMethod.PUT, HttpMethod.POST)); + + private final CacheProperties props; + + public FlushCache(CacheProperties props) { + this.props = props; + } + + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { + + if (responseContext.getStatus() >= 200 && responseContext.getStatus() <= 299) { + if (FlushCache.modifyMethods.contains(requestContext.getMethod())) { + + CacheManager cacheManager = Caching.getCachingProvider().getCacheManager( + Caching.getCachingProvider().getDefaultURI(), Thread.currentThread().getContextClassLoader()); + cacheManager.getCache(props.getCacheName()).remove( + new Key(requestContext.getUri(), requestContext.getAcceptableMediaTypes().get(0).toString())); + } + } + } + +} diff --git a/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/aai/AAIProperties.java b/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/aai/AAIProperties.java index ac8a6e6e52..9c7798f4c5 100644 --- a/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/aai/AAIProperties.java +++ b/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/aai/AAIProperties.java @@ -20,6 +20,7 @@ package org.onap.aaiclient.client.aai; +import org.onap.so.client.CacheProperties; import org.onap.so.client.RestProperties; public interface AAIProperties extends RestProperties { @@ -34,4 +35,15 @@ public interface AAIProperties extends RestProperties { public default boolean mapNotFoundToEmpty() { return true; } + + default CacheProperties getCacheProperties() { + return new AAICacheProperties() {}; + } + + public interface AAICacheProperties extends CacheProperties { + + default String getCacheName() { + return "aai-http-cache"; + } + } } diff --git a/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/graphinventory/GraphInventoryRestClient.java b/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/graphinventory/GraphInventoryRestClient.java index c2422085aa..c22f2f5f8e 100644 --- a/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/graphinventory/GraphInventoryRestClient.java +++ b/graph-inventory/aai-client/src/main/java/org/onap/aaiclient/client/graphinventory/GraphInventoryRestClient.java @@ -23,8 +23,13 @@ package org.onap.aaiclient.client.graphinventory; import java.net.URI; import java.util.Map; import java.util.Optional; +import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.core.Response; +import org.onap.aaiclient.client.CacheControlFeature; +import org.onap.aaiclient.client.FlushCache; import org.onap.logging.filter.base.ONAPComponentsList; +import org.onap.so.client.AddCacheHeaders; +import org.onap.so.client.CacheFactory; import org.onap.so.client.ResponseExceptionMapper; import org.onap.so.client.RestClientSSL; import org.onap.so.client.RestProperties; @@ -41,6 +46,21 @@ public abstract class GraphInventoryRestClient extends RestClientSSL { super(props, Optional.of(uri)); } + + protected ClientBuilder enableCaching(ClientBuilder builder) { + builder.register(new AddCacheHeaders(props.getCacheProperties())); + builder.register(new FlushCache(props.getCacheProperties())); + CacheControlFeature cacheControlFeature = new CacheControlFeature(); + cacheControlFeature.setCacheResponseInputStream(true); + cacheControlFeature.setExpiryPolicyFactory(new CacheFactory(props.getCacheProperties())); + builder.property("org.onap.aaiclient.client.CacheControlFeature.name", + props.getCacheProperties().getCacheName()); + + builder.register(cacheControlFeature); + + return builder; + } + @Override public abstract ONAPComponentsList getTargetEntity(); diff --git a/graph-inventory/aai-client/src/test/java/org/onap/aaiclient/client/aai/AAIRestClientTest.java b/graph-inventory/aai-client/src/test/java/org/onap/aaiclient/client/aai/AAIRestClientTest.java index b73454fe67..d0f7847726 100644 --- a/graph-inventory/aai-client/src/test/java/org/onap/aaiclient/client/aai/AAIRestClientTest.java +++ b/graph-inventory/aai-client/src/test/java/org/onap/aaiclient/client/aai/AAIRestClientTest.java @@ -25,7 +25,11 @@ import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.client.WireMock.put; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.ArgumentMatchers.any; @@ -35,8 +39,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import java.util.HashMap; import javax.ws.rs.core.Response; import org.junit.Rule; @@ -48,6 +54,7 @@ import org.mockito.junit.MockitoJUnitRunner; import org.onap.aaiclient.client.defaultproperties.DefaultAAIPropertiesImpl; import org.onap.aaiclient.client.graphinventory.GraphInventoryPatchConverter; import org.onap.aaiclient.client.graphinventory.exceptions.GraphInventoryPatchDepthExceededException; +import org.onap.so.client.RestClient; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.junit.WireMockRule; import com.google.common.collect.ImmutableMap; @@ -96,4 +103,113 @@ public class AAIRestClientTest { wireMockRule.verify(getRequestedFor(urlPathEqualTo("/test")).withHeader("X-FromAppId", equalTo("MSO")) .withHeader("X-TransactionId", matching(".*")).withHeader("test", equalTo("value"))); } + + + @Test + public void cacheGetTest() throws URISyntaxException, InterruptedException { + + wireMockRule.stubFor(get(urlPathMatching("/cached")) + .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "text/plain").withBody("value"))); + + AAIProperties props = new AAIProperties() { + + @Override + public URL getEndpoint() throws MalformedURLException { + return new URL(String.format("http://localhost:%s", wireMockRule.port())); + } + + @Override + public String getSystemName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isCachingEnabled() { + return true; + } + + @Override + public AAIVersion getDefaultVersion() { + return AAIVersion.LATEST; + } + + @Override + public String getAuth() { + return null; + } + + @Override + public String getKey() { + return null; + } + + }; + RestClient client = new AAIRestClient(props, new URI("/cached"), new HashMap()); + + Response response = client.get(); + + response.readEntity(String.class); + response = client.get(); + response.readEntity(String.class); + verify(1, getRequestedFor(urlEqualTo("/cached"))); + + } + + @Test + public void cachePutTest() throws URISyntaxException, InterruptedException { + + wireMockRule.stubFor(put(urlPathMatching("/cached/1")).willReturn(aResponse().withStatus(200))); + + wireMockRule.stubFor(get(urlPathMatching("/cached/1")) + .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{}"))); + + AAIProperties props = new AAIProperties() { + + @Override + public URL getEndpoint() throws MalformedURLException { + return new URL(String.format("http://localhost:%s", wireMockRule.port())); + } + + @Override + public String getSystemName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isCachingEnabled() { + return true; + } + + @Override + public AAIVersion getDefaultVersion() { + return AAIVersion.LATEST; + } + + @Override + public String getAuth() { + return null; + } + + @Override + public String getKey() { + return null; + } + + }; + + RestClient client = new AAIRestClient(props, new URI("/cached/1"), new HashMap()); + + + Response response = client.get(); + + response.readEntity(String.class); + client.put("wow"); + + client.get(); + response.readEntity(String.class); + verify(2, getRequestedFor(urlEqualTo("/cached/1"))); + + } } -- cgit 1.2.3-korg