Add json enricher and json field filter

This commit is contained in:
René Pfeuffer
2018-06-06 10:36:27 +02:00
parent aacb0b9e8d
commit 2c5823e961
24 changed files with 699 additions and 103 deletions

View File

@@ -35,16 +35,16 @@ package sonia.scm;
//~--- non-JDK imports --------------------------------------------------------
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Provider;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.name.Names;
import com.google.inject.servlet.RequestScoped;
import com.google.inject.servlet.ServletModule;
import com.google.inject.throwingproviders.ThrowingProviderBinder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.api.rest.ObjectMapperProvider;
import sonia.scm.cache.CacheManager;
import sonia.scm.cache.GuavaCacheManager;
import sonia.scm.config.ScmConfiguration;
@@ -56,18 +56,13 @@ import sonia.scm.group.GroupManagerProvider;
import sonia.scm.group.xml.XmlGroupDAO;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.io.FileSystem;
import sonia.scm.net.SSLContextProvider;
import sonia.scm.net.ahc.*;
import sonia.scm.plugin.DefaultPluginLoader;
import sonia.scm.plugin.DefaultPluginManager;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginManager;
import sonia.scm.repository.DefaultRepositoryManager;
import sonia.scm.repository.DefaultRepositoryProvider;
import sonia.scm.repository.HealthCheckContextListener;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryManagerProvider;
import sonia.scm.repository.RepositoryProvider;
import sonia.scm.repository.*;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.repository.spi.HookEventFacade;
@@ -76,28 +71,15 @@ import sonia.scm.resources.DefaultResourceManager;
import sonia.scm.resources.DevelopmentResourceManager;
import sonia.scm.resources.ResourceManager;
import sonia.scm.resources.ScriptResourceServlet;
import sonia.scm.security.CipherHandler;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.DefaultKeyGenerator;
import sonia.scm.security.DefaultSecuritySystem;
import sonia.scm.security.KeyGenerator;
import sonia.scm.security.SecuritySystem;
import sonia.scm.store.BlobStoreFactory;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.DataStoreFactory;
import sonia.scm.store.FileBlobStoreFactory;
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
import sonia.scm.store.JAXBDataStoreFactory;
import sonia.scm.store.JAXBConfigurationStoreFactory;
import sonia.scm.schedule.QuartzScheduler;
import sonia.scm.schedule.Scheduler;
import sonia.scm.security.*;
import sonia.scm.store.*;
import sonia.scm.template.MustacheTemplateEngine;
import sonia.scm.template.TemplateEngine;
import sonia.scm.template.TemplateEngineFactory;
import sonia.scm.template.TemplateServlet;
import sonia.scm.url.RestJsonUrlProvider;
import sonia.scm.url.RestXmlUrlProvider;
import sonia.scm.url.UrlProvider;
import sonia.scm.url.UrlProviderFactory;
import sonia.scm.url.WebUIUrlProvider;
import sonia.scm.url.*;
import sonia.scm.user.DefaultUserManager;
import sonia.scm.user.UserDAO;
import sonia.scm.user.UserManager;
@@ -105,31 +87,17 @@ import sonia.scm.user.UserManagerProvider;
import sonia.scm.user.xml.XmlUserDAO;
import sonia.scm.util.DebugServlet;
import sonia.scm.util.ScmConfigurationUtil;
import sonia.scm.web.UserAgentParser;
import sonia.scm.web.cgi.CGIExecutorFactory;
import sonia.scm.web.cgi.DefaultCGIExecutorFactory;
import sonia.scm.web.filter.LoggingFilter;
import sonia.scm.web.security.AdministrationContext;
import sonia.scm.web.security.DefaultAdministrationContext;
//~--- JDK imports ------------------------------------------------------------
import javax.servlet.ServletContext;
import sonia.scm.store.ConfigurationStoreFactory;
import javax.net.ssl.SSLContext;
import sonia.scm.net.SSLContextProvider;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.net.ahc.ContentTransformer;
import sonia.scm.net.ahc.DefaultAdvancedHttpClient;
import sonia.scm.net.ahc.JsonContentTransformer;
import sonia.scm.net.ahc.XmlContentTransformer;
import sonia.scm.schedule.QuartzScheduler;
import sonia.scm.schedule.Scheduler;
import sonia.scm.security.ConfigurableLoginAttemptHandler;
import sonia.scm.security.LoginAttemptHandler;
import sonia.scm.security.AuthorizationChangedEventProducer;
import sonia.scm.web.UserAgentParser;
import javax.servlet.ServletContext;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -354,6 +322,7 @@ public class ScmServletModule extends ServletModule
bind(TemplateEngine.class).annotatedWith(Default.class).to(
MustacheTemplateEngine.class);
bind(TemplateEngineFactory.class);
bind(ObjectMapper.class).toProvider(ObjectMapperProvider.class);
// bind events
// bind(LastModifiedUpdateListener.class);

View File

@@ -30,18 +30,9 @@
*/
package sonia.scm.api.rest;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
import javax.inject.Inject;
import javax.ws.rs.Produces;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
@@ -58,27 +49,13 @@ public final class JSONContextResolver implements ContextResolver<ObjectMapper>
private final ObjectMapper mapper;
public JSONContextResolver() {
mapper = new ObjectMapper()
.registerModule(new Jdk8Module())
.registerModule(new JavaTimeModule());
mapper.setAnnotationIntrospector(createAnnotationIntrospector());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true);
mapper.setDateFormat(new ISO8601DateFormat());
@Inject
public JSONContextResolver(ObjectMapper mapper) {
this.mapper = mapper;
}
private AnnotationIntrospector createAnnotationIntrospector() {
return new AnnotationIntrospectorPair(
new JaxbAnnotationIntrospector(TypeFactory.defaultInstance()),
new JacksonAnnotationIntrospector()
);
}
@Override
public ObjectMapper getContext(Class<?> type) {
return mapper;
}
}

View File

@@ -0,0 +1,40 @@
package sonia.scm.api.rest;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
import javax.inject.Provider;
import javax.inject.Singleton;
@Singleton
public class ObjectMapperProvider implements Provider<ObjectMapper> {
@Override
public ObjectMapper get() {
ObjectMapper mapper = new ObjectMapper()
.registerModule(new Jdk8Module())
.registerModule(new JavaTimeModule());
mapper.setAnnotationIntrospector(createAnnotationIntrospector());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true);
mapper.setDateFormat(new ISO8601DateFormat());
return mapper;
}
private AnnotationIntrospector createAnnotationIntrospector() {
return new AnnotationIntrospectorPair(
new JaxbAnnotationIntrospector(TypeFactory.defaultInstance()),
new JacksonAnnotationIntrospector()
);
}
}

View File

@@ -0,0 +1,70 @@
package sonia.scm.api.v2;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.Lists;
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import java.util.List;
import java.util.Optional;
@Provider
@Priority(Priorities.USER)
public class FieldContainerResponseFilter implements ContainerResponseFilter {
private static final String PARAMETER_FIELDS = "fields";
private static final String FIELD_SEPARATOR = ",";
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
Optional<JsonNode> entity = getJsonEntity(responseContext);
if (entity.isPresent()) {
List<String> fields = extractFieldsFrom(requestContext);
if (!fields.isEmpty()) {
JsonFilters.filterByFields(entity.get(), fields);
}
}
}
private Optional<JsonNode> getJsonEntity(ContainerResponseContext responseContext) {
Object entity = responseContext.getEntity();
if (isJsonEntity(entity)) {
return Optional.of((JsonNode) entity);
}
return Optional.empty();
}
private boolean isJsonEntity(Object entity) {
return entity instanceof JsonNode;
}
private List<String> extractFieldsFrom(ContainerRequestContext requestContext) {
List<String> fields = Lists.newArrayList();
List<String> fieldParameters = getFieldParameterFrom(requestContext);
if (fieldParameters != null && !fieldParameters.isEmpty()) {
for (String fieldParameter : fieldParameters) {
appendFieldsFromParameter(fields, fieldParameter);
}
}
return fields;
}
private List<String> getFieldParameterFrom(ContainerRequestContext requestContext) {
MultivaluedMap<String, String> queryParameters = requestContext.getUriInfo().getQueryParameters();
return queryParameters.get(PARAMETER_FIELDS);
}
private void appendFieldsFromParameter(List<String> fields, String fieldParameter) {
for (String field : fieldParameter.split(FIELD_SEPARATOR)) {
fields.add(field);
}
}
}

View File

@@ -0,0 +1,85 @@
package sonia.scm.api.v2;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Maps;
import java.util.Iterator;
import java.util.Map;
public final class JsonFilters {
private JsonFilters() {
}
public static void filterByFields(JsonNode root, Iterable<String> fields) {
filterNode(createJsonFilterNode(fields), root);
}
private static JsonFilterNode createJsonFilterNode(Iterable<String> fields) {
JsonFilterNode rootFilterNode = new JsonFilterNode();
for (String field : fields) {
appendFilterNode(rootFilterNode, field);
}
return rootFilterNode;
}
private static void appendFilterNode(JsonFilterNode rootFilterNode, String field) {
JsonFilterNode filterNode = rootFilterNode;
for (String part : field.split("\\.")) {
filterNode = filterNode.addOrGet(part);
}
}
private static void filterNode(JsonFilterNode filter, JsonNode node) {
if (node.isObject()) {
filterObjectNode(filter, (ObjectNode) node);
} else if (node.isArray()) {
filterArrayNode(filter, (ArrayNode) node);
}
}
private static void filterObjectNode(JsonFilterNode filter, ObjectNode objectNode) {
Iterator<Map.Entry<String,JsonNode>> entryIterator = objectNode.fields();
while (entryIterator.hasNext()) {
Map.Entry<String,JsonNode> entry = entryIterator.next();
JsonFilterNode childFilter = filter.get(entry.getKey());
if (childFilter == null) {
entryIterator.remove();
} else if (!childFilter.isLeaf()) {
filterNode(childFilter, entry.getValue());
}
}
}
private static void filterArrayNode(JsonFilterNode filter, ArrayNode arrayNode) {
for (int i=0; i<arrayNode.size(); i++) {
filterNode(filter, arrayNode.get(i));
}
}
private static class JsonFilterNode {
private final Map<String,JsonFilterNode> children = Maps.newHashMap();
JsonFilterNode addOrGet(String name) {
JsonFilterNode child = children.get(name);
if (child == null) {
child = new JsonFilterNode();
children.put(name, child);
}
return child;
}
JsonFilterNode get(String name) {
return children.get(name);
}
boolean isLeaf() {
return children.isEmpty();
}
}
}

View File

@@ -0,0 +1,60 @@
package sonia.scm.api.v2;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import sonia.scm.web.JsonEnricher;
import sonia.scm.web.JsonEnricherContext;
import sonia.scm.web.VndMediaType;
import javax.annotation.Priority;
import javax.inject.Inject;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
import java.util.Set;
@Provider
@Priority(Priorities.USER + 1000)
public class JsonMarshallingResponseFilter implements ContainerResponseFilter {
private final ObjectMapper objectMapper;
private final Set<JsonEnricher> enrichers;
@Inject
public JsonMarshallingResponseFilter(ObjectMapper objectMapper, Set<JsonEnricher> enrichers) {
this.objectMapper = objectMapper;
this.enrichers = enrichers;
}
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
if (hasVndEntity(responseContext)) {
JsonNode node = getJsonEntity(responseContext);
callEnrichers(requestContext, responseContext, node);
responseContext.setEntity(node);
}
}
private void callEnrichers(ContainerRequestContext requestContext, ContainerResponseContext responseContext, JsonNode node) {
JsonEnricherContext context = new JsonEnricherContext(
requestContext.getUriInfo().getRequestUri(),
responseContext.getMediaType(),
node
);
for (JsonEnricher enricher : enrichers) {
enricher.enrich(context);
}
}
private JsonNode getJsonEntity(ContainerResponseContext responseContext) {
Object entity = responseContext.getEntity();
return objectMapper.valueToTree(entity);
}
private boolean hasVndEntity(ContainerResponseContext responseContext) {
return responseContext.hasEntity() && VndMediaType.isVndType(responseContext.getMediaType());
}
}

View File

@@ -1,9 +1,4 @@
package sonia.scm.api.v2.resources;
public class ScmMediaType {
private static final String VERSION = "2";
private static final String PREFIX = "application/vnd.scmm-";
private static final String SUFFIX = "+json;v=" + VERSION;
public static final String USER = PREFIX + "user" + SUFFIX;
}

View File

@@ -11,6 +11,7 @@ import sonia.scm.api.rest.resources.AbstractManagerResource;
import sonia.scm.user.User;
import sonia.scm.user.UserException;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
@@ -20,10 +21,9 @@ import java.util.List;
import java.util.stream.Collectors;
import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging;
import static sonia.scm.api.v2.resources.ScmMediaType.USER;
@Singleton
@Produces(USER)
@Produces(VndMediaType.USER)
public class UserCollectionResource extends AbstractManagerResource<User, UserException> {
public static final int DEFAULT_PAGE_SIZE = 10;
private final UserDto2UserMapper dtoToUserMapper;

View File

@@ -11,15 +11,15 @@ import sonia.scm.security.Role;
import sonia.scm.user.User;
import sonia.scm.user.UserException;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.util.Collection;
import static sonia.scm.api.v2.resources.ScmMediaType.USER;
@Singleton
@Produces(USER)
@Produces(VndMediaType.USER)
public class UserSubResource extends AbstractManagerResource<User, UserException> {
private final UserDto2UserMapper dtoToUserMapper;
private final User2UserDtoMapper userToDtoMapper;