mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 08:25:44 +01:00
Add json enricher and json field filter
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
85
scm-webapp/src/main/java/sonia/scm/api/v2/JsonFilters.java
Normal file
85
scm-webapp/src/main/java/sonia/scm/api/v2/JsonFilters.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user