Commit 1beff785 authored by Martin Lowe's avatar Martin Lowe

Updates to add GraphQL support while maintaining backwards compat

WIP, more updates to come as support is fully ironed out.
Signed-off-by: Martin Lowe's avatarMartin Lowe <martin.lowe@eclipse-foundation.org>
parent 9a43522e
......@@ -3,7 +3,7 @@
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<artifactId>eclipsefdn-java-sdk-core</artifactId>
<name>Core - Runtime</name>
<name>Core - Runtime</name>
<parent>
<groupId>org.eclipsefoundation</groupId>
<artifactId>eclipsefdn-java-sdk-commons</artifactId>
......@@ -52,6 +52,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.logmanager</groupId>
<artifactId>jboss-logmanager</artifactId>
......
package org.eclipsefoundation.core.helper;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.ws.rs.core.MultivaluedMap;
import org.eclipsefoundation.core.namespace.UrlParameterNamespace;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import io.quarkus.runtime.Startup;
/**
* Helper for parameter lookups and filtering.
*
* @author Martin Lowe
*
*/
@Startup
@ApplicationScoped
public class ParameterHelper {
@Inject
Instance<UrlParameterNamespace> urlParamImpls;
// maintain internal list of key names to reduce churn. These are only created
// at build time
private List<String> urlParamNames;
/**
* Create the list of parameter key names once upon creation of this class
*/
@PostConstruct
void initialize() {
// use array list for better random access + lookups
this.urlParamNames = new ArrayList<>();
// populate a list with each
urlParamImpls.forEach(
namespace -> namespace.getParameters().stream().forEach(param -> urlParamNames.add(param.getName())));
}
/**
* Filters out unknown parameters by name/key. Creates and returns a new map
* with the known/tracked parameters. Tracked parameters are added through
* implementations of the {@link UrlParameterNamespace} interface.
*
* @param src multivalued parameter map to filter
* @return a map containing the filtered parameter values
*/
public MultivaluedMap<String, String> filterUnknownParameters(MultivaluedMap<String, String> src) {
MultivaluedMap<String, String> out = new MultivaluedMapImpl<>();
Set<String> keys = src.keySet();
for (String key : keys) {
if (urlParamNames.contains(key)) {
out.addAll(key, src.get(key));
}
}
return out;
}
/**
* Returns a list of parameter names, as generated from reading in the
* parameters present in the {@link UrlParameterNamespace} implementations.
*
* @return list of tracked parameter names.
*/
public List<String> getValidParameters() {
return new ArrayList<>(urlParamNames);
}
}
......@@ -17,6 +17,7 @@ import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.json.bind.Jsonb;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.xml.bind.DatatypeConverter;
......@@ -42,23 +43,25 @@ public class ResponseHelper {
throw new RuntimeException("Could not create an MD5 hash digest");
}
}
@Inject
Jsonb jsonb;
@Inject
CachingService<?> cachingService;
CachingService cachingService;
/**
* Builds a response using passed data. Uses references to the caching service
* and the current request to add information about ETags and Cache-Control
* headers.
*
* @param id the ID of the object to be stored in cache
* @param wrapper the query parameters for the current request
* @param data the data to attach to the response
* @param id the ID of the object to be stored in cache
* @param wrapper the query parameters for the current request
* @param data the data to attach to the response
* @param cachingService the cache for the current data request.
* @return a complete response object for the given data and request.
*/
public Response build(String id, RequestWrapper wrapper, Object data) {
public Response build(String id, RequestWrapper wrapper, MultivaluedMap<String, String> optParams, Object data,
Class<?> type) {
// set default cache control flags for API responses
CacheControl cc = new CacheControl();
cc.setNoStore(wrapper.isCacheBypass());
......@@ -66,7 +69,7 @@ public class ResponseHelper {
if (!cc.isNoStore()) {
cc.setMaxAge((int) cachingService.getMaxAge());
// get the TTL for the current entry
Optional<Long> ttl = cachingService.getExpiration(id, wrapper);
Optional<Long> ttl = cachingService.getExpiration(id, optParams == null ? wrapper.asMap() : optParams, type);
if (!ttl.isPresent()) {
return Response.serverError().build();
}
......@@ -80,7 +83,7 @@ public class ResponseHelper {
hash = DatatypeConverter.printHexBinary(DIGEST.digest());
DIGEST.reset();
}
// check if etag matches
String etag = wrapper.getHeader("Etag");
if (hash.equals(etag)) {
......
......@@ -16,8 +16,10 @@ import java.util.Objects;
import java.util.Optional;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriInfo;
import org.eclipsefoundation.core.namespace.DeprecatedHeader;
......@@ -25,8 +27,10 @@ import org.eclipsefoundation.core.namespace.RequestHeaderNames;
import org.eclipsefoundation.core.namespace.UrlParameterNamespace.UrlParameter;
import org.eclipsefoundation.core.request.CacheBypassFilter;
import org.jboss.resteasy.core.ResteasyContext;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import io.quarkus.arc.Unremovable;
import io.smallrye.graphql.api.Context;
/**
* Wrapper class for query parameter functionality, wrapping a Map of String to
......@@ -43,6 +47,9 @@ public class RequestWrapper {
private Map<String, List<String>> params;
@Inject
Context context;
private UriInfo uriInfo;
private HttpServletRequest request;
private HttpServletResponse response;
......@@ -135,8 +142,10 @@ public class RequestWrapper {
*
* @return a copy of the internal param map
*/
public Map<String, List<String>> asMap() {
return new HashMap<>(getParams());
public MultivaluedMap<String, String> asMap() {
MultivaluedMap<String, String> out = new MultivaluedMapImpl<>();
getParams().forEach(out::addAll);
return out;
}
private Map<String, List<String>> getParams() {
......@@ -155,7 +164,13 @@ public class RequestWrapper {
* @return
*/
public String getEndpoint() {
return uriInfo.getPath();
String endpoint = "";
if (uriInfo != null) {
endpoint = uriInfo.getPath();
} else if (context != null) {
endpoint = context.getPath();
}
return endpoint;
}
/**
......
......@@ -6,12 +6,7 @@
*/
package org.eclipsefoundation.core.resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
......@@ -35,28 +30,24 @@ import org.jboss.resteasy.annotations.jaxrs.PathParam;
public class CacheResource {
@Inject
Instance<CachingService<?>> cacheServices;
CachingService cacheService;
@GET
public Response getActiveCacheEntries() {
List<Set<String>> cacheEntries = new ArrayList<>();
for (CachingService<?> cs : cacheServices) {
cacheEntries.add(cs.getCacheKeys());
}
return Response.ok(cacheEntries).build();
return Response.ok(cacheService.getCacheKeys()).build();
}
@DELETE
@Path("/{key}")
public Response removeCacheEntry(@PathParam("key") String key) {
cacheServices.forEach(cs -> cs.remove(key));
cacheService.remove(key);
return Response.ok().build();
}
@DELETE
@Path("/all")
public Response clearCaches() {
cacheServices.forEach(CachingService::removeAll);
cacheService.removeAll();
return Response.ok().build();
}
}
......@@ -42,7 +42,7 @@ public class PaginatedResultsFilter implements ContainerResponseFilter {
getArrayLimitedNumber(listEntity, defaultPageSize * page)));
// add link headers for paginated page hints
UriBuilder builder = getUriInfo().getBaseUriBuilder();
UriBuilder builder = getUriInfo().getRequestUriBuilder();
LinkHeader lh = new LinkHeader();
// add first + last page link headers
lh.addLink("this page of results", "self", buildHref(builder, page), "");
......
......@@ -10,7 +10,9 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import org.eclipsefoundation.core.model.RequestWrapper;
import javax.ws.rs.core.MultivaluedMap;
import org.eclipsefoundation.core.helper.ParameterHelper;
import io.quarkus.arc.Unremovable;
......@@ -21,7 +23,7 @@ import io.quarkus.arc.Unremovable;
* @param <T> the type of object to be stored in the cache.
*/
@Unremovable
public interface CachingService<T> {
public interface CachingService {
/**
* Returns an Optional object of type T, returning a cached object if available,
......@@ -31,9 +33,13 @@ public interface CachingService<T> {
* @param id the ID of the object to be stored in cache
* @param params the query parameters for the current request
* @param callable a runnable that returns an object of type T
* @param rawType the rawtype of data returned, which may be different than the
* actual data type returned (such as a returned list containing
* the raw type).
* @return the cached result
*/
Optional<T> get(String id, RequestWrapper params, Callable<? extends T> callable);
<T> Optional<T> get(String id, MultivaluedMap<String, String> params, Class<?> rawType,
Callable<? extends T> callable);
/**
* Returns the expiration date in millis since epoch.
......@@ -43,7 +49,7 @@ public interface CachingService<T> {
* @return an Optional expiration date for the current object if its set. If
* there is no underlying data, then empty would be returned
*/
Optional<Long> getExpiration(String id, RequestWrapper params);
Optional<Long> getExpiration(String id, MultivaluedMap<String, String> params, Class<?> type);
/**
* @return the max age of cache entries
......@@ -69,25 +75,37 @@ public interface CachingService<T> {
*/
void removeAll();
/**
* Promise presence of ParameterHelper to facilitate the generation of the cache
* key.
*
* @return instance of the ParameterHelper class.
*/
ParameterHelper getParameterHelper();
/**
* Generates a unique key based on the id of the item/set of items to be stored,
* as well as any passed parameters.
* as well as any passed parameters. This includes the passed map of parameters
* that may override what is present in the request.
*
* @param id identity string of the item to cache
* @param wrapper parameters associated with the request for information
* @param params the map of override parameters for the current request
* @param type type of object to be cached
* @return the unique cache key for the request.
*/
default String getCacheKey(String id, RequestWrapper wrapper) {
default String getCacheKey(String id, MultivaluedMap<String, String> params, Class<?> type) {
StringBuilder sb = new StringBuilder();
sb.append('[').append(wrapper.getEndpoint()).append(']');
sb.append('[').append(type.getSimpleName()).append(']');
sb.append("id:").append(id);
// filter the parameters using a reference to the parameter helper
ParameterHelper paramHelper = getParameterHelper();
MultivaluedMap<String, String> filteredParams = paramHelper.filterUnknownParameters(params);
// join all the non-empty params to the key to create distinct entries for
// filtered values
wrapper.asMap().entrySet().stream().filter(e -> !e.getValue().isEmpty())
.map(e -> e.getKey() + '=' + String.join(",", e.getValue()))
.forEach(s -> sb.append('|').append(s));
filteredParams.entrySet().stream().filter(e -> !e.getValue().isEmpty())
.map(e -> e.getKey() + '=' + String.join(",", e.getValue())).forEach(s -> sb.append('|').append(s));
return sb.toString();
}
}
......@@ -7,9 +7,11 @@ import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.core.MultivaluedMap;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipsefoundation.core.model.RequestWrapper;
import org.eclipsefoundation.core.helper.ParameterHelper;
import org.eclipsefoundation.core.namespace.MicroprofilePropertyNames;
import org.eclipsefoundation.core.service.CachingService;
import org.jboss.resteasy.spi.NotImplementedYetException;
......@@ -29,7 +31,7 @@ import io.quarkus.cache.CacheResult;
* @param <T>
*/
@ApplicationScoped
public class QuarkusCachingService<T> implements CachingService<T> {
public class QuarkusCachingService implements CachingService {
private static final Logger LOGGER = LoggerFactory.getLogger(QuarkusCachingService.class);
@ConfigProperty(name = MicroprofilePropertyNames.CACHE_SIZE_MAX, defaultValue = "10000")
......@@ -37,22 +39,26 @@ public class QuarkusCachingService<T> implements CachingService<T> {
@ConfigProperty(name = MicroprofilePropertyNames.CACHE_TTL_MAX_SECONDS, defaultValue = "900")
long ttlWrite;
@Inject
ParameterHelper paramHelper;
@Override
public Optional<T> get(String id, RequestWrapper wrapper, Callable<? extends T> callable) {
Objects.requireNonNull(id);
Objects.requireNonNull(wrapper);
public <T> Optional<T> get(String id, MultivaluedMap<String, String> params, Class<?> rawType,
Callable<? extends T> callable) {
Objects.requireNonNull(callable);
String cacheKey = getCacheKey(id, wrapper);
String cacheKey = getCacheKey(Objects.requireNonNull(id), Objects.requireNonNull(params),
Objects.requireNonNull(rawType));
LOGGER.debug("Retrieving cache value for '{}'", cacheKey);
return Optional.ofNullable(get(cacheKey, callable));
}
@Override
public Optional<Long> getExpiration(String id, RequestWrapper params) {
String cacheKey = getCacheKey(Objects.requireNonNull(id), Objects.requireNonNull(params));
public Optional<Long> getExpiration(String id, MultivaluedMap<String, String> params, Class<?> type) {
String cacheKey = getCacheKey(Objects.requireNonNull(id), Objects.requireNonNull(params),
Objects.requireNonNull(type));
try {
return Optional.ofNullable(getExpiration(cacheKey, true));
return Optional.ofNullable(getExpiration(true, cacheKey));
} catch (Exception e) {
throw new RuntimeException("Error while retrieving expiration for cachekey: " + cacheKey);
}
......@@ -93,7 +99,7 @@ public class QuarkusCachingService<T> implements CachingService<T> {
public Long checkExpiration(String cacheKey) {
try {
// check for existing value, throwing out if none is found.
return getExpiration(cacheKey, true);
return getExpiration(true, cacheKey);
} catch (Exception e) {
// no result found
return null;
......@@ -101,12 +107,12 @@ public class QuarkusCachingService<T> implements CachingService<T> {
}
@CacheResult(cacheName = "default")
private T get(@CacheKey String cacheKey, Callable<? extends T> callable) {
public <T> T get(@CacheKey String cacheKey, Callable<? extends T> callable) {
T out = null;
try {
out = callable.call();
LOGGER.debug("Created cache entry with key '{}' that expires at '{}'", cacheKey,
getExpiration(cacheKey, false));
// set internal expiration cache
getExpiration(false, cacheKey);
} catch (Exception e) {
LOGGER.error("Error while creating cache entry for key '{}': \n", cacheKey, e);
}
......@@ -114,11 +120,17 @@ public class QuarkusCachingService<T> implements CachingService<T> {
}
@CacheResult(cacheName = "ttl")
private long getExpiration(@CacheKey String cacheKey, boolean throwIfMissing) throws Exception {
public long getExpiration(boolean throwIfMissing, @CacheKey String cacheKey) throws Exception {
if (throwIfMissing) {
throw new Exception("No TTL present for cache key '{}', not generating");
}
LOGGER.error("Timeout for {}: {}", cacheKey, System.currentTimeMillis() + getMaxAge());
LOGGER.debug("Timeout for {}: {}", cacheKey, System.currentTimeMillis() + getMaxAge());
return System.currentTimeMillis() + getMaxAge();
}
@Override
public ParameterHelper getParameterHelper() {
return paramHelper;
}
}
......@@ -5,8 +5,6 @@ eclipse.oauth.override=false
## LOGGER CONFIG
quarkus.log.file.enable=true
quarkus.log.file.level=INFO
quarkus.log.level=TRACE
quarkus.log.file.path=/tmp/logs/quarkus.log
# MISC
......
......@@ -46,6 +46,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-oauth2-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-graphql-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache-deployment</artifactId>
......
......@@ -7,7 +7,6 @@
package org.eclipsefoundation.persistence.dao;
import java.util.List;
import java.util.concurrent.CompletionStage;
import javax.enterprise.event.Observes;
......@@ -42,7 +41,7 @@ public interface PersistenceDao extends HealthCheck {
* @param documents the list of typed documents to add to the database instance.
* @return a future Void result indicating success on return.
*/
<T extends BareNode> void add(RDBMSQuery<T> q, List<T> documents);
<T extends BareNode> List<T> add(RDBMSQuery<T> q, List<T> documents);
/**
* Deletes documents that match the given query.
......@@ -57,12 +56,11 @@ public interface PersistenceDao extends HealthCheck {
/**
* Counts the number of filtered results of the given document type present.
*
* @param <T> the type of documents beign counted
* @param q the query object for the current operation
* @return a future long result representing the number of results available for
* the given query and docuement type.
* @param q the query object for the current operation
* @return a long result representing the number of results available for the
* given query and docuement type.
*/
<T extends BareNode> CompletionStage<Long> count(RDBMSQuery<T> q);
Long count(RDBMSQuery<?> q);
default void startup(@Observes StartupEvent event) {
// intentionally empty
......
......@@ -6,8 +6,8 @@
*/
package org.eclipsefoundation.persistence.dao.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletionStage;
import javax.inject.Inject;
import javax.persistence.EntityManager;
......@@ -52,7 +52,7 @@ public class DefaultHibernateDao implements PersistenceDao {
throw new MaintenanceException();
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Querying DB using the following query: {}", q);
LOGGER.debug("Querying DB using the following query: {}", q.getFilter().getSelectSql());
}
// build base query
......@@ -76,7 +76,7 @@ public class DefaultHibernateDao implements PersistenceDao {
@Transactional
@Override
public <T extends BareNode> void add(RDBMSQuery<T> q, List<T> documents) {
public <T extends BareNode> List<T> add(RDBMSQuery<T> q, List<T> documents) {
if (maintenanceFlag) {
throw new MaintenanceException();
}
......@@ -84,12 +84,14 @@ public class DefaultHibernateDao implements PersistenceDao {
LOGGER.debug("Adding {} documents to DB of type {}", documents.size(), q.getDocType().getSimpleName());
}
// for each doc, check if update or create
List<T> updatedDocs = new ArrayList<>(documents.size());
for (T doc : documents) {
T ref = doc;
if (doc.getId() != null) {
// ensure this object exists before merging on it
if (em.find(q.getDocType(), doc.getId()) != null) {
LOGGER.debug("Merging document with existing document with id '{}'", doc.getId());
em.merge(doc);
ref = em.merge(doc);
} else {
LOGGER.debug("Persisting new document with id '{}'", doc.getId());
em.persist(doc);
......@@ -98,7 +100,10 @@ public class DefaultHibernateDao implements PersistenceDao {
LOGGER.debug("Persisting new document with generated UUID ID");
em.persist(doc);
}
// add the ref to the output list
updatedDocs.add(ref);
}
return updatedDocs;
}
@Transactional
......@@ -122,14 +127,24 @@ public class DefaultHibernateDao implements PersistenceDao {
@Transactional
@Override
public <T extends BareNode> CompletionStage<Long> count(RDBMSQuery<T> q) {
public Long count(RDBMSQuery<?> q) {
if (maintenanceFlag) {
throw new MaintenanceException();
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Counting documents in DB that match the following query: {}", q);
LOGGER.debug("Counting documents in DB that match the following query: {}", q.getFilter().getCountSql());
}
throw new RuntimeException("Not yet supported");