From ab1466b131dbc24c0a8b98d5caeffb477bd19b05 Mon Sep 17 00:00:00 2001 From: Martin Lowe Date: Wed, 15 Jun 2022 08:50:25 -0400 Subject: [PATCH 1/5] Add optional cache viewer resource to core implementation --- core/pom.xml | 11 ++ .../core/namespace/OptionalPath.java | 21 +++ .../core/request/OptionalPathFilter.java | 57 ++++++ .../core/resource/CacheResource.java | 167 ++++++++++++++++++ .../core/template/MapToJSONExtension.java | 17 ++ .../main/resources/templates/cache_keys.html | 57 ++++++ .../resources/templates/eclipse_footer.html | 62 +++++++ .../resources/templates/eclipse_header.html | 90 ++++++++++ persistence/deployment/pom.xml | 4 + 9 files changed, 486 insertions(+) create mode 100644 core/src/main/java/org/eclipsefoundation/core/namespace/OptionalPath.java create mode 100644 core/src/main/java/org/eclipsefoundation/core/request/OptionalPathFilter.java create mode 100644 core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java create mode 100644 core/src/main/java/org/eclipsefoundation/core/template/MapToJSONExtension.java create mode 100644 core/src/main/resources/templates/cache_keys.html create mode 100644 core/src/main/resources/templates/eclipse_footer.html create mode 100644 core/src/main/resources/templates/eclipse_header.html diff --git a/core/pom.xml b/core/pom.xml index 0b0009b..b4fa21c 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -46,6 +46,17 @@ org.apache.commons commons-lang3 + + + commons-collections + commons-collections + 3.2.2 + + + + io.quarkus + quarkus-resteasy-qute + io.quarkus diff --git a/core/src/main/java/org/eclipsefoundation/core/namespace/OptionalPath.java b/core/src/main/java/org/eclipsefoundation/core/namespace/OptionalPath.java new file mode 100644 index 0000000..8ac8160 --- /dev/null +++ b/core/src/main/java/org/eclipsefoundation/core/namespace/OptionalPath.java @@ -0,0 +1,21 @@ +package org.eclipsefoundation.core.namespace; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(METHOD) +public @interface OptionalPath { + /** + * Name of the configuration value to check for whether the targeted resource path should be allowed to continue + * processing. + * + * @return the full path of the configuration value for enabling/disabling the given endpoint. + */ + String value(); + + boolean enabledByDefault() default false; +} diff --git a/core/src/main/java/org/eclipsefoundation/core/request/OptionalPathFilter.java b/core/src/main/java/org/eclipsefoundation/core/request/OptionalPathFilter.java new file mode 100644 index 0000000..07ac36d --- /dev/null +++ b/core/src/main/java/org/eclipsefoundation/core/request/OptionalPathFilter.java @@ -0,0 +1,57 @@ +package org.eclipsefoundation.core.request; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Optional; + +import javax.enterprise.inject.Instance; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipsefoundation.core.namespace.OptionalPath; +import org.jboss.resteasy.core.interception.jaxrs.PostMatchContainerRequestContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Does lookups on the current path to see if the resource is disabled. + * + * @author Martin Lowe + * + */ +@Provider +public class OptionalPathFilter implements ContainerRequestFilter { + public static final Logger LOGGER = LoggerFactory.getLogger(OptionalPathFilter.class); + + @ConfigProperty(name = "eclipse.optional-resources.enabled", defaultValue = "false") + Instance optionalResourcesEnabled; + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + if (optionalResourcesEnabled.get()) { + // check annotation on target endpoint to be sure that endpoint is enabled + Method m = ((PostMatchContainerRequestContext) requestContext).getResourceMethod().getMethod(); + OptionalPath opt = m.getAnnotation(OptionalPath.class); + if (opt != null) { + // get the specified config value fresh and check that it is enabled, or enabled by default if missing + Optional configValue = ConfigProvider.getConfig().getOptionalValue(opt.value(), Boolean.class); + if (configValue.isPresent() && configValue.get()) { + LOGGER.trace("Request to '{}' enabled by config, allowing call", + requestContext.getUriInfo().getAbsolutePath()); + } else if (configValue.isEmpty() && opt.enabledByDefault()) { + LOGGER.trace("Request to '{}' enabled by default, allowing call", + requestContext.getUriInfo().getAbsolutePath()); + } else { + LOGGER.trace("Request to '{}' rejected as endpoint is not enabled", + requestContext.getUriInfo().getAbsolutePath()); + // abort with 404 as we should hide that this exists + requestContext.abortWith(Response.status(404).build()); + } + } + } + } +} diff --git a/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java b/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java new file mode 100644 index 0000000..1b393b2 --- /dev/null +++ b/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java @@ -0,0 +1,167 @@ +package org.eclipsefoundation.core.resource; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.eclipsefoundation.core.namespace.OptionalPath; +import org.eclipsefoundation.core.service.CachingService; +import org.eclipsefoundation.core.service.CachingService.ParameterizedCacheKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.auto.value.AutoValue; + +import io.quarkus.qute.Location; +import io.quarkus.qute.Template; +import io.quarkus.runtime.Startup; + +@Path("/caches") +@Produces(MediaType.APPLICATION_JSON) +public class CacheResource { + static final Logger LOGGER = LoggerFactory.getLogger(CacheResource.class); + + @Inject + InstanceCacheResourceKey key; + + // Qute templates, generates email bodies + @Location("cache_keys") + Template cacheKeysView; + + @Inject + CachingService service; + + @GET + @Path("keys") + @Produces(MediaType.TEXT_HTML) + @OptionalPath("eclipse.cache.resource.enabled") + public Response getCaches(@QueryParam("key") String passedKey) { + if (shouldBlockCacheRequest(passedKey)) { + return Response.status(403).build(); + } + return Response.ok() + .entity(cacheKeysView.data("cacheKeys", + service.getCacheKeys().stream().map(this::buildWrappedKeys).collect(Collectors.toList()), "key", + passedKey).render()) + .build(); + } + + @POST + @Path("{cacheKey}/clear") + @OptionalPath("eclipse.cache.resource.enabled") + public Response clearForKey(@PathParam("cacheKey") String cacheKey, @QueryParam("key") String passedKey, + Map> params) { + if (shouldBlockCacheRequest(passedKey)) { + return Response.status(403).build(); + } + // remove the given keys from the cache + List keys = service.getCacheKeys().stream() + .filter(k -> k.getId().equals(cacheKey) && checkParams(params, k.getParams())) + .collect(Collectors.toList()); + keys.forEach(k -> service.remove(k)); + return Response.ok().entity(keys.size()).build(); + } + + /** + * Converts cache keys to wrapped cache keys with their expiration time. + * + * @param k the key to wrap + * @return the wrapped key, containing the passed key and its time to live. + */ + private WrappedCacheKey buildWrappedKeys(ParameterizedCacheKey k) { + return WrappedCacheKey.builder() + .setTtl(new Date( + service.getExpiration(k.getId(), k.getParams(), k.getClazz()).orElse(service.getMaxAge()))) + .setKey(k).build(); + } + + /** + * Compare 2 parameter collections to check if they match. + * + * @param passedParams the passed params we need to match + * @param entryParams the current entry to checks params + * @return true if they match (insensitive to order), false otherwise + */ + private boolean checkParams(Map> passedParams, MultivaluedMap entryParams) { + if (passedParams.size() != entryParams.size()) { + return false; + } + return passedParams.entrySet().stream() + .allMatch(e -> CollectionUtils.isEqualCollection(e.getValue(), entryParams.get(e.getKey()))); + } + + /** + * Checks the access key to see if the current request should be blocked. + * + * @param passedKey the key passed by the requester + * @return true if request should be blocked, false otherwise. + */ + private boolean shouldBlockCacheRequest(String passedKey) { + return StringUtils.isBlank(passedKey) || !passedKey.equals(key.key); + } + + /** + * Cache key plus its expiration to be displayed on the view more page. + * + * @author Martin Lowe + * + */ + @AutoValue + @JsonDeserialize(builder = AutoValue_CacheResource_WrappedCacheKey.Builder.class) + public static abstract class WrappedCacheKey { + public abstract ParameterizedCacheKey getKey(); + + public abstract Date getTtl(); + + public static Builder builder() { + return new AutoValue_CacheResource_WrappedCacheKey.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setKey(ParameterizedCacheKey key); + + public abstract Builder setTtl(Date ttl); + + public abstract WrappedCacheKey build(); + } + } + + /** + * Contains the random key generated on system start. Creates a random secure key that will be used to gate access + * to the cache resources endpoint. + * + * @author Martin Lowe + * + */ + @Startup + @Singleton + static class InstanceCacheResourceKey { + static final Logger LOGGER = LoggerFactory.getLogger(InstanceCacheResourceKey.class); + final String key; + + InstanceCacheResourceKey() { + key = UUID.randomUUID().toString(); + LOGGER.info("Generated cache resource key: {}", key); + } + } +} diff --git a/core/src/main/java/org/eclipsefoundation/core/template/MapToJSONExtension.java b/core/src/main/java/org/eclipsefoundation/core/template/MapToJSONExtension.java new file mode 100644 index 0000000..c45feca --- /dev/null +++ b/core/src/main/java/org/eclipsefoundation/core/template/MapToJSONExtension.java @@ -0,0 +1,17 @@ +package org.eclipsefoundation.core.template; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.ws.rs.core.MultivaluedMap; + +import io.quarkus.qute.TemplateExtension; + +@TemplateExtension +public class MapToJSONExtension { + public static JsonObject toJson(MultivaluedMap params) { + JsonObjectBuilder out = Json.createObjectBuilder(); + params.forEach((k, v) -> out.add(k, Json.createArrayBuilder(v))); + return out.build(); + } +} diff --git a/core/src/main/resources/templates/cache_keys.html b/core/src/main/resources/templates/cache_keys.html new file mode 100644 index 0000000..c154f24 --- /dev/null +++ b/core/src/main/resources/templates/cache_keys.html @@ -0,0 +1,57 @@ +{#include eclipse_header /} +{||} + +
+

Cache keys

+ + + + + + + + + + {#for entry in cacheKeys.orEmpty} + + + + + + + + {/for} + +
IDParamsTypeTTLActions
{entry.key.id}{entry.key.params.toJson}{entry.key.clazz.getSimpleName}{entry.ttl} + Clear +
+
+{||} +{#include eclipse_footer /} \ No newline at end of file diff --git a/core/src/main/resources/templates/eclipse_footer.html b/core/src/main/resources/templates/eclipse_footer.html new file mode 100644 index 0000000..e9d8470 --- /dev/null +++ b/core/src/main/resources/templates/eclipse_footer.html @@ -0,0 +1,62 @@ + +

+ Back to the top +

+
+ + + + + diff --git a/core/src/main/resources/templates/eclipse_header.html b/core/src/main/resources/templates/eclipse_header.html new file mode 100644 index 0000000..b03554d --- /dev/null +++ b/core/src/main/resources/templates/eclipse_header.html @@ -0,0 +1,90 @@ + + + + + + + +{||} + + + + HTML Template | The Eclipse Foundation + + + + + + + + + + + + + + + + + + {||} + + + + Skip to main content +
+
+
+
+ + +
+
+
+
\ No newline at end of file diff --git a/persistence/deployment/pom.xml b/persistence/deployment/pom.xml index 6151f59..1b45f11 100644 --- a/persistence/deployment/pom.xml +++ b/persistence/deployment/pom.xml @@ -54,6 +54,10 @@ io.quarkus quarkus-jdbc-mariadb-deployment
+ + io.quarkus + quarkus-resteasy-qute-deployment + -- GitLab From 71784346f83e620eae485abf45493f9a452ebdbe Mon Sep 17 00:00:00 2001 From: Martin Lowe Date: Wed, 15 Jun 2022 11:23:43 -0400 Subject: [PATCH 2/5] Add tests + static namespace vars for property names for opt resources --- .../core/request/OptionalPathFilter.java | 43 ++++++++------- .../core/resource/CacheResource.java | 5 +- .../resource/CacheResourceDisabledTest.java | 30 +++++++++++ .../core/resource/CacheResourceTest.java | 54 +++++++++++++++++++ .../OptionalResourceEnabledTestProfile.java | 30 +++++++++++ 5 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 core/src/test/java/org/eclipsefoundation/core/resource/CacheResourceDisabledTest.java create mode 100644 core/src/test/java/org/eclipsefoundation/core/resource/CacheResourceTest.java create mode 100644 core/src/test/java/org/eclipsefoundation/core/test/OptionalResourceEnabledTestProfile.java diff --git a/core/src/main/java/org/eclipsefoundation/core/request/OptionalPathFilter.java b/core/src/main/java/org/eclipsefoundation/core/request/OptionalPathFilter.java index 07ac36d..e62e001 100644 --- a/core/src/main/java/org/eclipsefoundation/core/request/OptionalPathFilter.java +++ b/core/src/main/java/org/eclipsefoundation/core/request/OptionalPathFilter.java @@ -32,26 +32,31 @@ public class OptionalPathFilter implements ContainerRequestFilter { @Override public void filter(ContainerRequestContext requestContext) throws IOException { - if (optionalResourcesEnabled.get()) { - // check annotation on target endpoint to be sure that endpoint is enabled - Method m = ((PostMatchContainerRequestContext) requestContext).getResourceMethod().getMethod(); - OptionalPath opt = m.getAnnotation(OptionalPath.class); - if (opt != null) { - // get the specified config value fresh and check that it is enabled, or enabled by default if missing - Optional configValue = ConfigProvider.getConfig().getOptionalValue(opt.value(), Boolean.class); - if (configValue.isPresent() && configValue.get()) { - LOGGER.trace("Request to '{}' enabled by config, allowing call", - requestContext.getUriInfo().getAbsolutePath()); - } else if (configValue.isEmpty() && opt.enabledByDefault()) { - LOGGER.trace("Request to '{}' enabled by default, allowing call", - requestContext.getUriInfo().getAbsolutePath()); - } else { - LOGGER.trace("Request to '{}' rejected as endpoint is not enabled", - requestContext.getUriInfo().getAbsolutePath()); - // abort with 404 as we should hide that this exists - requestContext.abortWith(Response.status(404).build()); - } + // check annotation on target endpoint to be sure that endpoint is enabled + Method m = ((PostMatchContainerRequestContext) requestContext).getResourceMethod().getMethod(); + OptionalPath opt = m.getAnnotation(OptionalPath.class); + if (opt != null) { + if (!optionalResourcesEnabled.get()) { + LOGGER.trace("Request to '{}' rejected as optional resources are not enabled", + requestContext.getUriInfo().getAbsolutePath()); + // abort with 404 as we should hide that this exists + requestContext.abortWith(Response.status(404).build()); + } + // get the specified config value fresh and check that it is enabled, or enabled by default if missing + Optional configValue = ConfigProvider.getConfig().getOptionalValue(opt.value(), Boolean.class); + if (configValue.isPresent() && configValue.get()) { + LOGGER.trace("Request to '{}' enabled by config, allowing call", + requestContext.getUriInfo().getAbsolutePath()); + } else if (configValue.isEmpty() && opt.enabledByDefault()) { + LOGGER.trace("Request to '{}' enabled by default, allowing call", + requestContext.getUriInfo().getAbsolutePath()); + } else { + LOGGER.trace("Request to '{}' rejected as endpoint is not enabled", + requestContext.getUriInfo().getAbsolutePath()); + // abort with 404 as we should hide that this exists + requestContext.abortWith(Response.status(404).build()); } } + } } diff --git a/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java b/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java index 1b393b2..fd293b7 100644 --- a/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java +++ b/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java @@ -20,6 +20,7 @@ import javax.ws.rs.core.Response; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.eclipsefoundation.core.namespace.MicroprofilePropertyNames; import org.eclipsefoundation.core.namespace.OptionalPath; import org.eclipsefoundation.core.service.CachingService; import org.eclipsefoundation.core.service.CachingService.ParameterizedCacheKey; @@ -52,7 +53,7 @@ public class CacheResource { @GET @Path("keys") @Produces(MediaType.TEXT_HTML) - @OptionalPath("eclipse.cache.resource.enabled") + @OptionalPath(MicroprofilePropertyNames.CACHE_RESOURCE_ENABLED) public Response getCaches(@QueryParam("key") String passedKey) { if (shouldBlockCacheRequest(passedKey)) { return Response.status(403).build(); @@ -66,7 +67,7 @@ public class CacheResource { @POST @Path("{cacheKey}/clear") - @OptionalPath("eclipse.cache.resource.enabled") + @OptionalPath(MicroprofilePropertyNames.CACHE_RESOURCE_ENABLED) public Response clearForKey(@PathParam("cacheKey") String cacheKey, @QueryParam("key") String passedKey, Map> params) { if (shouldBlockCacheRequest(passedKey)) { diff --git a/core/src/test/java/org/eclipsefoundation/core/resource/CacheResourceDisabledTest.java b/core/src/test/java/org/eclipsefoundation/core/resource/CacheResourceDisabledTest.java new file mode 100644 index 0000000..2093aa7 --- /dev/null +++ b/core/src/test/java/org/eclipsefoundation/core/resource/CacheResourceDisabledTest.java @@ -0,0 +1,30 @@ +package org.eclipsefoundation.core.resource; + +import static io.restassured.RestAssured.given; + +import java.util.UUID; + +import javax.inject.Inject; + +import org.eclipsefoundation.core.resource.CacheResource.InstanceCacheResourceKey; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class CacheResourceDisabledTest { + + @Inject + InstanceCacheResourceKey key; + + @Test + void cacheUI_protected() { + given().when().get("/caches/keys").then().statusCode(404); + } + + @Test + void cacheUI_returnsMissingWithKeyValues() { + given().when().get("/caches/keys?key={key}", UUID.randomUUID().toString()).then().statusCode(404); + given().when().get("/caches/keys?key={key}", key.key).then().statusCode(404); + } +} diff --git a/core/src/test/java/org/eclipsefoundation/core/resource/CacheResourceTest.java b/core/src/test/java/org/eclipsefoundation/core/resource/CacheResourceTest.java new file mode 100644 index 0000000..0270103 --- /dev/null +++ b/core/src/test/java/org/eclipsefoundation/core/resource/CacheResourceTest.java @@ -0,0 +1,54 @@ +package org.eclipsefoundation.core.resource; + +import static io.restassured.RestAssured.given; + +import java.util.UUID; + +import javax.inject.Inject; + +import org.eclipsefoundation.core.resource.CacheResource.InstanceCacheResourceKey; +import org.eclipsefoundation.core.test.OptionalResourceEnabledTestProfile; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.arc.Arc; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(OptionalResourceEnabledTestProfile.class) +public class CacheResourceTest { + + @Inject + InstanceCacheResourceKey key; + + @Test + void resourceSecuringKey_changesEachCreation() { + InstanceCacheResourceKey k = new InstanceCacheResourceKey(); + InstanceCacheResourceKey k2 = new InstanceCacheResourceKey(); + Assertions.assertNotEquals(k.key, k2.key); + } + + @Test + void resourceSecuringKey_availableThroughCDI() { + Assertions.assertNotNull(key.key); + } + + @Test + void resourceSecuringKey_sharedLifecycle() { + // check that key doesn't change across calls + InstanceCacheResourceKey keyCdiRef = Arc.container().instance(InstanceCacheResourceKey.class).get(); + Assertions.assertEquals(keyCdiRef.key, key.key); + } + + @Test + void cacheUI_protected() { + given().when().get("/caches/keys").then().statusCode(403); + } + + @Test + void cacheUI_usesInstanceKey() { + given().when().get("/caches/keys?key={key}", UUID.randomUUID().toString()).then().statusCode(403); + given().when().get("/caches/keys?key={key}", key.key).then().statusCode(200); + } +} diff --git a/core/src/test/java/org/eclipsefoundation/core/test/OptionalResourceEnabledTestProfile.java b/core/src/test/java/org/eclipsefoundation/core/test/OptionalResourceEnabledTestProfile.java new file mode 100644 index 0000000..ca6ed3b --- /dev/null +++ b/core/src/test/java/org/eclipsefoundation/core/test/OptionalResourceEnabledTestProfile.java @@ -0,0 +1,30 @@ +package org.eclipsefoundation.core.test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.eclipsefoundation.core.namespace.MicroprofilePropertyNames; + +import io.quarkus.test.junit.QuarkusTestProfile; + +/** + * + * @author Martin Lowe + */ +public class OptionalResourceEnabledTestProfile implements QuarkusTestProfile { + + // private immutable copy of the configs for auth state + private static final Map CONFIG_OVERRIDES; + static { + Map tmp = new HashMap<>(); + tmp.put(MicroprofilePropertyNames.OPTIONAL_RESOURCES_ENABLED, "true"); + tmp.put(MicroprofilePropertyNames.CACHE_RESOURCE_ENABLED, "true"); + CONFIG_OVERRIDES = Collections.unmodifiableMap(tmp); + } + + @Override + public Map getConfigOverrides() { + return CONFIG_OVERRIDES; + } +} -- GitLab From ae848bc88d5bfba99dbcc9b9dbafbd0529130d67 Mon Sep 17 00:00:00 2001 From: Martin Lowe Date: Mon, 28 Nov 2022 13:50:12 -0500 Subject: [PATCH 3/5] Update startup proxy service to resolve issues with structure Startup proxy service broke 2-3 of our development patterns and it was missed in review. These issues were fixed as part of this PR and will be included in next version. --- .../core/{service => model}/StartupProxy.java | 2 +- .../core/service/StartupProxyService.java | 57 ++++++++----------- .../impl/DefaultStartupProxyService.java | 55 ++++++++++++++++++ .../core/test/services/FooBarService.java | 2 +- 4 files changed, 81 insertions(+), 35 deletions(-) rename core/src/main/java/org/eclipsefoundation/core/{service => model}/StartupProxy.java (95%) create mode 100644 core/src/main/java/org/eclipsefoundation/core/service/impl/DefaultStartupProxyService.java diff --git a/core/src/main/java/org/eclipsefoundation/core/service/StartupProxy.java b/core/src/main/java/org/eclipsefoundation/core/model/StartupProxy.java similarity index 95% rename from core/src/main/java/org/eclipsefoundation/core/service/StartupProxy.java rename to core/src/main/java/org/eclipsefoundation/core/model/StartupProxy.java index 2583a57..ba72c0f 100644 --- a/core/src/main/java/org/eclipsefoundation/core/service/StartupProxy.java +++ b/core/src/main/java/org/eclipsefoundation/core/model/StartupProxy.java @@ -9,7 +9,7 @@ * * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ -package org.eclipsefoundation.core.service; +package org.eclipsefoundation.core.model; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/core/src/main/java/org/eclipsefoundation/core/service/StartupProxyService.java b/core/src/main/java/org/eclipsefoundation/core/service/StartupProxyService.java index acde361..e1cf9a8 100644 --- a/core/src/main/java/org/eclipsefoundation/core/service/StartupProxyService.java +++ b/core/src/main/java/org/eclipsefoundation/core/service/StartupProxyService.java @@ -1,41 +1,32 @@ -/********************************************************************* -* Copyright (c) 2022 Eclipse Foundation. -* -* This program and the accompanying materials are made -* available under the terms of the Eclipse Public License 2.0 -* which is available at https://www.eclipse.org/legal/epl-2.0/ -* -* Author: Zachary Sabourin -* -* SPDX-License-Identifier: EPL-2.0 -**********************************************************************/ +/** + * Copyright (c) 2022 Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * Author: Martin Lowe + * + * SPDX-License-Identifier: EPL-2.0 + */ package org.eclipsefoundation.core.service; -import javax.annotation.PostConstruct; -import javax.enterprise.context.ApplicationScoped; -import javax.enterprise.inject.Instance; -import javax.inject.Inject; +import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.quarkus.runtime.Startup; +import org.eclipsefoundation.core.model.StartupProxy; /** - * Finds services extending StartupProxy and ensures they are loaded at startup. - * This ensures that beans served using the provider pattern are found and - * default values are loaded. + * Interface for binding classes that can't have startup annotations to initialize at startup. + * + * @author Martin Lowe + * */ -@Startup -@ApplicationScoped -public class StartupProxyService { - static final Logger LOGGER = LoggerFactory.getLogger(StartupProxyService.class); - - @Inject - Instance services; +public interface StartupProxyService { - @PostConstruct - void init() { - services.forEach(StartupProxy::startupProxy); - } + /** + * Returns a list of proxied bean classes. + * + * @return list of beans detected as using the startup proxy. + */ + List> getProxiedBeanTypes(); } diff --git a/core/src/main/java/org/eclipsefoundation/core/service/impl/DefaultStartupProxyService.java b/core/src/main/java/org/eclipsefoundation/core/service/impl/DefaultStartupProxyService.java new file mode 100644 index 0000000..ad72e5d --- /dev/null +++ b/core/src/main/java/org/eclipsefoundation/core/service/impl/DefaultStartupProxyService.java @@ -0,0 +1,55 @@ +/********************************************************************* +* Copyright (c) 2022 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* Author: Zachary Sabourin +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.core.service.impl; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; + +import org.eclipsefoundation.core.model.StartupProxy; +import org.eclipsefoundation.core.service.StartupProxyService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.quarkus.runtime.Startup; + +/** + * Finds services extending StartupProxy and ensures they are loaded at startup. This ensures that beans served using + * the provider pattern are found and default values are loaded. + * + * To properly bind in cases where the services are managed by a bean provider class, the interface of the service + * should be extended with the StartupProxy, rather than marking the actual implementation with an additional implements + * call. + */ +@Startup +@ApplicationScoped +public class DefaultStartupProxyService implements StartupProxyService { + static final Logger LOGGER = LoggerFactory.getLogger(DefaultStartupProxyService.class); + + @Inject + Instance services; + + @PostConstruct + void init() { + services.forEach(StartupProxy::startupProxy); + } + + @Override + public List> getProxiedBeanTypes() { + return services.stream().map(StartupProxy::getClass).collect(Collectors.toList()); + } + +} diff --git a/core/src/test/java/org/eclipsefoundation/core/test/services/FooBarService.java b/core/src/test/java/org/eclipsefoundation/core/test/services/FooBarService.java index 08e82c9..6605641 100644 --- a/core/src/test/java/org/eclipsefoundation/core/test/services/FooBarService.java +++ b/core/src/test/java/org/eclipsefoundation/core/test/services/FooBarService.java @@ -11,7 +11,7 @@ **********************************************************************/ package org.eclipsefoundation.core.test.services; -import org.eclipsefoundation.core.service.StartupProxy; +import org.eclipsefoundation.core.model.StartupProxy; public interface FooBarService extends StartupProxy { String getName(); -- GitLab From 2d2e027c438bc370bf97cebd1ccaeeca9133b9ba Mon Sep 17 00:00:00 2001 From: Martin Lowe Date: Mon, 28 Nov 2022 13:51:11 -0500 Subject: [PATCH 4/5] Update cache key to use a header to post the updates rather query param --- .../eclipsefoundation/core/resource/CacheResource.java | 9 +++++---- core/src/main/resources/templates/cache_keys.html | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java b/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java index fd293b7..856ff34 100644 --- a/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java +++ b/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java @@ -9,6 +9,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -43,7 +44,7 @@ public class CacheResource { @Inject InstanceCacheResourceKey key; - // Qute templates, generates email bodies + // Qute templates, generates cache key interface @Location("cache_keys") Template cacheKeysView; @@ -68,7 +69,7 @@ public class CacheResource { @POST @Path("{cacheKey}/clear") @OptionalPath(MicroprofilePropertyNames.CACHE_RESOURCE_ENABLED) - public Response clearForKey(@PathParam("cacheKey") String cacheKey, @QueryParam("key") String passedKey, + public Response clearForKey(@PathParam("cacheKey") String cacheKey, @HeaderParam("x-cache-key") String passedKey, Map> params) { if (shouldBlockCacheRequest(passedKey)) { return Response.status(403).build(); @@ -127,7 +128,7 @@ public class CacheResource { */ @AutoValue @JsonDeserialize(builder = AutoValue_CacheResource_WrappedCacheKey.Builder.class) - public static abstract class WrappedCacheKey { + public abstract static class WrappedCacheKey { public abstract ParameterizedCacheKey getKey(); public abstract Date getTtl(); @@ -161,7 +162,7 @@ public class CacheResource { final String key; InstanceCacheResourceKey() { - key = UUID.randomUUID().toString(); + this.key = UUID.randomUUID().toString(); LOGGER.info("Generated cache resource key: {}", key); } } diff --git a/core/src/main/resources/templates/cache_keys.html b/core/src/main/resources/templates/cache_keys.html index c154f24..fb18994 100644 --- a/core/src/main/resources/templates/cache_keys.html +++ b/core/src/main/resources/templates/cache_keys.html @@ -41,12 +41,13 @@ let k = ctx.getAttribute('data-key'); let id = ctx.getAttribute('data-id'); let p = ctx.getAttribute('data-params'); - const response = await fetch(`${id}/clear?key=${k}`, { + const response = await fetch(`${id}/clear`, { method: 'POST', cache: 'no-cache', credentials: 'same-origin', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'x-cache-key': k }, redirect: 'follow', body: p -- GitLab From 4d41347d77617b6cd5347f48a51bbf56e064967f Mon Sep 17 00:00:00 2001 From: Martin Lowe Date: Wed, 30 Nov 2022 08:49:50 -0500 Subject: [PATCH 5/5] Update cache resource for feedback, fix minor issues with PR Pagination was causing failures for native endpoints which were used in verifying some of the endpoints downstream. Added refresh or alert when clearing cache keys --- .../core/resource/CacheResource.java | 36 +++++++++++------- .../core/response/PaginatedResultsFilter.java | 37 +++++++++++++------ .../service/impl/QuarkusCachingService.java | 21 +++++------ .../main/resources/templates/cache_keys.html | 8 +++- .../resources/templates/eclipse_header.html | 19 +--------- 5 files changed, 66 insertions(+), 55 deletions(-) diff --git a/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java b/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java index 856ff34..3bc7b4c 100644 --- a/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java +++ b/core/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; +import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; import javax.ws.rs.GET; @@ -59,10 +60,12 @@ public class CacheResource { if (shouldBlockCacheRequest(passedKey)) { return Response.status(403).build(); } - return Response.ok() - .entity(cacheKeysView.data("cacheKeys", - service.getCacheKeys().stream().map(this::buildWrappedKeys).collect(Collectors.toList()), "key", - passedKey).render()) + return Response + .ok() + .entity(cacheKeysView + .data("cacheKeys", service.getCacheKeys().stream().map(this::buildWrappedKeys).collect(Collectors.toList()), "key", + passedKey) + .render()) .build(); } @@ -75,7 +78,9 @@ public class CacheResource { return Response.status(403).build(); } // remove the given keys from the cache - List keys = service.getCacheKeys().stream() + List keys = service + .getCacheKeys() + .stream() .filter(k -> k.getId().equals(cacheKey) && checkParams(params, k.getParams())) .collect(Collectors.toList()); keys.forEach(k -> service.remove(k)); @@ -89,10 +94,13 @@ public class CacheResource { * @return the wrapped key, containing the passed key and its time to live. */ private WrappedCacheKey buildWrappedKeys(ParameterizedCacheKey k) { - return WrappedCacheKey.builder() - .setTtl(new Date( - service.getExpiration(k.getId(), k.getParams(), k.getClazz()).orElse(service.getMaxAge()))) - .setKey(k).build(); + Date d = null; + try { + d = new Date(service.getExpiration(k.getId(), k.getParams(), k.getClazz()).orElse(service.getMaxAge())); + } catch (RuntimeException e) { + // intentionally empty + } + return WrappedCacheKey.builder().setTtl(d).setKey(k).build(); } /** @@ -106,8 +114,7 @@ public class CacheResource { if (passedParams.size() != entryParams.size()) { return false; } - return passedParams.entrySet().stream() - .allMatch(e -> CollectionUtils.isEqualCollection(e.getValue(), entryParams.get(e.getKey()))); + return passedParams.entrySet().stream().allMatch(e -> CollectionUtils.isEqualCollection(e.getValue(), entryParams.get(e.getKey()))); } /** @@ -131,6 +138,7 @@ public class CacheResource { public abstract static class WrappedCacheKey { public abstract ParameterizedCacheKey getKey(); + @Nullable public abstract Date getTtl(); public static Builder builder() { @@ -142,15 +150,15 @@ public class CacheResource { public abstract static class Builder { public abstract Builder setKey(ParameterizedCacheKey key); - public abstract Builder setTtl(Date ttl); + public abstract Builder setTtl(@Nullable Date ttl); public abstract WrappedCacheKey build(); } } /** - * Contains the random key generated on system start. Creates a random secure key that will be used to gate access - * to the cache resources endpoint. + * Contains the random key generated on system start. Creates a random secure key that will be used to gate access to + * the cache resources endpoint. * * @author Martin Lowe * diff --git a/core/src/main/java/org/eclipsefoundation/core/response/PaginatedResultsFilter.java b/core/src/main/java/org/eclipsefoundation/core/response/PaginatedResultsFilter.java index 23fd873..94f4320 100644 --- a/core/src/main/java/org/eclipsefoundation/core/response/PaginatedResultsFilter.java +++ b/core/src/main/java/org/eclipsefoundation/core/response/PaginatedResultsFilter.java @@ -76,19 +76,14 @@ public class PaginatedResultsFilter implements ContainerResponseFilter { ResourceInfo resourceInfo; @Override - public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) - throws IOException { - - Method method = resourceInfo.getResourceMethod(); - Pagination annotation = method.getAnnotation(Pagination.class); + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { - if ((annotation == null || annotation.value()) && Boolean.TRUE.equals(enablePagination.get())) { + if (checkPaginationAnnotation() && Boolean.TRUE.equals(enablePagination.get())) { Object entity = responseContext.getEntity(); // only try and paginate if there are multiple entities if (entity instanceof Set) { - paginateResults(responseContext, - (new TreeSet<>((Set) entity)).stream().collect(Collectors.toList())); + paginateResults(responseContext, (new TreeSet<>((Set) entity)).stream().collect(Collectors.toList())); } else if (entity instanceof List) { paginateResults(responseContext, (List) entity); } @@ -110,13 +105,31 @@ public class PaginatedResultsFilter implements ContainerResponseFilter { if (listEntity.size() > pageSize) { // set the sliced array as the entity responseContext - .setEntity(listEntity.subList(getArrayLimitedNumber(listEntity, Math.max(0, page - 1) * pageSize), - getArrayLimitedNumber(listEntity, pageSize * page))); + .setEntity(listEntity + .subList(getArrayLimitedNumber(listEntity, Math.max(0, page - 1) * pageSize), + getArrayLimitedNumber(listEntity, pageSize * page))); } // set the link header to the response responseContext.getHeaders().add("Link", createLinkHeader(page, lastPage)); } + /** + * Checks the current method for pagination annotations and if present, checks to make sure pagination is enabled. + * Defaults to using pagination if the pagination annotation is missing or cannot be read. + * + * @return true if pagination should be enabled, false otherwise + */ + private boolean checkPaginationAnnotation() { + + // method can be null sometimes for quarkus native endpoints + Method method = resourceInfo.getResourceMethod(); + if (method != null) { + Pagination annotation = method.getAnnotation(Pagination.class); + return annotation == null || annotation.value(); + } + return true; + } + /** * Gets a header value if available, checking first the servlet response then the mutable response wrapper. * @@ -230,8 +243,8 @@ public class PaginatedResultsFilter implements ContainerResponseFilter { * * @param list the list to bind the number by * @param num the number to check for exceeding bounds. - * @return the passed number if its within the size of the given array, 0 if the number is negative, and the array - * size if greater than the maximum bounds. + * @return the passed number if its within the size of the given array, 0 if the number is negative, and the array size + * if greater than the maximum bounds. */ private int getArrayLimitedNumber(List list, int num) { return Math.min(list.size(), Math.max(0, num)); diff --git a/core/src/main/java/org/eclipsefoundation/core/service/impl/QuarkusCachingService.java b/core/src/main/java/org/eclipsefoundation/core/service/impl/QuarkusCachingService.java index dbc03ae..31d7fbb 100644 --- a/core/src/main/java/org/eclipsefoundation/core/service/impl/QuarkusCachingService.java +++ b/core/src/main/java/org/eclipsefoundation/core/service/impl/QuarkusCachingService.java @@ -39,7 +39,8 @@ import io.quarkus.cache.CacheResult; import io.quarkus.cache.CaffeineCache; /** - * Utililzes Quarkus caching extensions in order to cache and retrieve data. + * Utililzes Quarkus caching extensions in order to cache and retrieve data. Reason we augment over the Quarkus core + * cache (caffeine) is so that we can record TTLs for cache objects which aren't exposed by base cache. * * @author Martin Lowe * @@ -58,12 +59,10 @@ public class QuarkusCachingService implements CachingService { Cache cache; @Override - public Optional get(String id, MultivaluedMap params, Class rawType, - Callable callable) { + public Optional get(String id, MultivaluedMap params, Class rawType, Callable callable) { Objects.requireNonNull(callable); - ParameterizedCacheKey cacheKey = new ParameterizedCacheKey(rawType, id, - params != null ? params : new MultivaluedMapImpl<>()); + ParameterizedCacheKey cacheKey = new ParameterizedCacheKey(rawType, id, params != null ? params : new MultivaluedMapImpl<>()); LOGGER.debug("Retrieving cache value for '{}'", cacheKey); return Optional.ofNullable(get(cacheKey, callable)); } @@ -101,8 +100,7 @@ public class QuarkusCachingService implements CachingService { @Override public void fuzzyRemove(String id, Class type) { - this.getCacheKeys().stream().filter(k -> k.getId().equals(id) && k.getClazz().equals(type)) - .forEach(this::remove); + this.getCacheKeys().stream().filter(k -> k.getId().equals(id) && k.getClazz().equals(type)).forEach(this::remove); } @Override @@ -136,21 +134,22 @@ public class QuarkusCachingService implements CachingService { @CacheResult(cacheName = "default") public T get(@CacheKey ParameterizedCacheKey cacheKey, Callable callable) { + // attempt to get the result for the cache entry T out = null; try { out = callable.call(); - // set internal expiration cache - getExpiration(false, cacheKey); } catch (Exception e) { LOGGER.error("Error while creating cache entry for key '{}': \n", cacheKey, e); } + // need to make sure that the expiration is always set for a cache value since we accept null + getExpiration(false, cacheKey); return out; } @CacheResult(cacheName = "ttl") - public long getExpiration(boolean throwIfMissing, @CacheKey ParameterizedCacheKey cacheKey) throws Exception { + public long getExpiration(boolean throwIfMissing, @CacheKey ParameterizedCacheKey cacheKey) { if (throwIfMissing) { - throw new Exception("No TTL present for cache key '{}', not generating"); + throw new RuntimeException("No TTL present for cache key '{}', not generating"); } LOGGER.debug("Timeout for {}: {}", cacheKey, System.currentTimeMillis() + getMaxAge()); return System.currentTimeMillis() + getMaxAge(); diff --git a/core/src/main/resources/templates/cache_keys.html b/core/src/main/resources/templates/cache_keys.html index fb18994..440156c 100644 --- a/core/src/main/resources/templates/cache_keys.html +++ b/core/src/main/resources/templates/cache_keys.html @@ -27,7 +27,7 @@ {entry.key.id} {entry.key.params.toJson} {entry.key.clazz.getSimpleName} - {entry.ttl} + {entry.ttl or "Unknown"} Clear @@ -51,6 +51,12 @@ }, redirect: 'follow', body: p + }).then(r => { + if (r.status ===200 ) { + location.reload(); + } else { + alert(`Could not clear cache entry for key ${id}`); + } }); } diff --git a/core/src/main/resources/templates/eclipse_header.html b/core/src/main/resources/templates/eclipse_header.html index b03554d..d01c046 100644 --- a/core/src/main/resources/templates/eclipse_header.html +++ b/core/src/main/resources/templates/eclipse_header.html @@ -33,24 +33,9 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= {||} - - - Skip to main content + Skip to main content
-
-
-
- - -
-
-
+
-- GitLab