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

@@ -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;