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

@@ -484,6 +484,7 @@
<jaxrs.version>2.0.1</jaxrs.version> <jaxrs.version>2.0.1</jaxrs.version>
<jersey-client.version>1.19.4</jersey-client.version> <jersey-client.version>1.19.4</jersey-client.version>
<jackson.version>2.8.6</jackson.version>
<guice.version>4.0</guice.version> <guice.version>4.0</guice.version>
<!-- event bus --> <!-- event bus -->

View File

@@ -82,7 +82,17 @@
<artifactId>javax.ws.rs-api</artifactId> <artifactId>javax.ws.rs-api</artifactId>
<version>${jaxrs.version}</version> <version>${jaxrs.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- event bus --> <!-- event bus -->
<dependency> <dependency>

View File

@@ -0,0 +1,10 @@
package sonia.scm.web;
import sonia.scm.plugin.ExtensionPoint;
@ExtensionPoint
public interface JsonEnricher {
void enrich(JsonEnricherContext context);
}

View File

@@ -0,0 +1,31 @@
package sonia.scm.web;
import com.fasterxml.jackson.databind.JsonNode;
import javax.ws.rs.core.MediaType;
import java.net.URI;
public class JsonEnricherContext {
private URI requestUri;
private MediaType responseMediaType;
private JsonNode responseEntity;
public JsonEnricherContext(URI requestUri, MediaType responseMediaType, JsonNode responseEntity) {
this.requestUri = requestUri;
this.responseMediaType = responseMediaType;
this.responseEntity = responseEntity;
}
public URI getRequestUri() {
return requestUri;
}
public MediaType getResponseMediaType() {
return responseMediaType;
}
public JsonNode getResponseEntity() {
return responseEntity;
}
}

View File

@@ -0,0 +1,28 @@
package sonia.scm.web;
import javax.ws.rs.core.MediaType;
public class VndMediaType {
private static final String VERSION = "2";
private static final String TYPE = "application";
private static final String SUBTYPE_PREFIX = "vnd.scmm-";
private static final String PREFIX = TYPE + "/" + SUBTYPE_PREFIX;
private static final String SUFFIX = "+json;v=" + VERSION;
public static final String USER = PREFIX + "user" + SUFFIX;
private VndMediaType() {
}
public static MediaType jsonType(String resource) {
return MediaType.valueOf(json(resource));
}
public static String json(String resource) {
return PREFIX + resource + SUFFIX;// ".v2+json";
}
public static boolean isVndType(MediaType type) {
return type.getType().equals(TYPE) && type.getSubtype().startsWith(SUBTYPE_PREFIX);
}
}

View File

@@ -560,7 +560,6 @@
<wagon.version>1.0</wagon.version> <wagon.version>1.0</wagon.version>
<mustache.version>0.8.17</mustache.version> <mustache.version>0.8.17</mustache.version>
<resteasy.version>3.1.3.Final</resteasy.version> <resteasy.version>3.1.3.Final</resteasy.version>
<jackson.version>2.8.6</jackson.version>
<netbeans.hint.deploy.server>Tomcat</netbeans.hint.deploy.server> <netbeans.hint.deploy.server>Tomcat</netbeans.hint.deploy.server>
<sonar.issue.ignore.multicriteria>e1</sonar.issue.ignore.multicriteria> <sonar.issue.ignore.multicriteria>e1</sonar.issue.ignore.multicriteria>
<sonar.issue.ignore.multicriteria.e1.ruleKey>javascript:S3827</sonar.issue.ignore.multicriteria.e1.ruleKey> <sonar.issue.ignore.multicriteria.e1.ruleKey>javascript:S3827</sonar.issue.ignore.multicriteria.e1.ruleKey>

View File

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

View File

@@ -30,18 +30,9 @@
*/ */
package sonia.scm.api.rest; 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.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.Produces;
import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider; import javax.ws.rs.ext.Provider;
@@ -58,27 +49,13 @@ public final class JSONContextResolver implements ContextResolver<ObjectMapper>
private final ObjectMapper mapper; private final ObjectMapper mapper;
public JSONContextResolver() { @Inject
mapper = new ObjectMapper() public JSONContextResolver(ObjectMapper mapper) {
.registerModule(new Jdk8Module()) this.mapper = mapper;
.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());
} }
private AnnotationIntrospector createAnnotationIntrospector() {
return new AnnotationIntrospectorPair(
new JaxbAnnotationIntrospector(TypeFactory.defaultInstance()),
new JacksonAnnotationIntrospector()
);
}
@Override @Override
public ObjectMapper getContext(Class<?> type) { public ObjectMapper getContext(Class<?> type) {
return mapper; 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; package sonia.scm.api.v2.resources;
public class ScmMediaType { 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.User;
import sonia.scm.user.UserException; import sonia.scm.user.UserException;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.*; import javax.ws.rs.*;
import javax.ws.rs.core.*; import javax.ws.rs.core.*;
@@ -20,10 +21,9 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging; import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging;
import static sonia.scm.api.v2.resources.ScmMediaType.USER;
@Singleton @Singleton
@Produces(USER) @Produces(VndMediaType.USER)
public class UserCollectionResource extends AbstractManagerResource<User, UserException> { public class UserCollectionResource extends AbstractManagerResource<User, UserException> {
public static final int DEFAULT_PAGE_SIZE = 10; public static final int DEFAULT_PAGE_SIZE = 10;
private final UserDto2UserMapper dtoToUserMapper; private final UserDto2UserMapper dtoToUserMapper;

View File

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

View File

@@ -59,7 +59,7 @@ import static org.junit.Assert.*;
*/ */
public class JSONContextResolverTest { public class JSONContextResolverTest {
private final ObjectMapper mapper = new JSONContextResolver().getContext(Object.class); private final ObjectMapper mapper = new ObjectMapperProvider().get();
/** /**
* Tests json unmarshalling with unknown properties. * Tests json unmarshalling with unknown properties.

View File

@@ -0,0 +1,85 @@
package sonia.scm.api.v2;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.google.common.io.Resources;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URL;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class FieldContainerResponseFilterTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Mock
private ContainerRequestContext requestContext;
@Mock
private ContainerResponseContext responseContext;
private FieldContainerResponseFilter filter = new FieldContainerResponseFilter();
@Test
public void testFilter() throws IOException {
applyFields("one");
JsonNode node = applyEntity("filter-test-002");
filter.filter(requestContext, responseContext);
assertEquals("{\"one\":1}", objectMapper.writeValueAsString(node));
}
@Test
public void testFilterWithMultiple() throws IOException {
applyFields("one", "five");
JsonNode node = applyEntity("filter-test-002");
filter.filter(requestContext, responseContext);
assertEquals("{\"one\":1,\"five\":5}", objectMapper.writeValueAsString(node));
}
@Test
public void testFilterCommaSeparated() throws IOException {
applyFields("one,five");
JsonNode node = applyEntity("filter-test-002");
filter.filter(requestContext, responseContext);
assertEquals("{\"one\":1,\"five\":5}", objectMapper.writeValueAsString(node));
}
private void applyFields(String... fields) {
UriInfo info = mock(UriInfo.class);
MultivaluedMap<String,String> queryParameters = mock(MultivaluedMap.class);
when(queryParameters.get("fields")).thenReturn(Lists.newArrayList(fields));
when(info.getQueryParameters()).thenReturn(queryParameters);
when(requestContext.getUriInfo()).thenReturn(info);
}
private JsonNode applyEntity(String name) throws IOException {
JsonNode node = readJson(name);
when(responseContext.hasEntity()).thenReturn(Boolean.TRUE);
when(responseContext.getEntity()).thenReturn(node);
return node;
}
private JsonNode readJson(String name) throws IOException {
URL resource = Resources.getResource("sonia/scm/api/v2/" + name + ".json");
return objectMapper.readTree(resource);
}
}

View File

@@ -0,0 +1,83 @@
package sonia.scm.api.v2;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.collect.Lists;
import com.google.common.io.Resources;
import org.junit.Test;
import java.io.IOException;
import java.net.URL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class JsonFiltersTest {
private ObjectMapper objectMapper = new ObjectMapper();
@Test
public void testFilterByFields() throws IOException {
JsonNode node = readJson("filter-test-001");
JsonFilters.filterByFields(node, Lists.newArrayList("one"));
assertEquals(1, node.get("one").intValue());
assertFalse(node.has("two"));
assertFalse(node.has("three"));
}
@Test
public void testFilterByFieldsWithMultipleFields() throws IOException {
JsonNode node = readJson("filter-test-001");
JsonFilters.filterByFields(node, Lists.newArrayList("one", "three"));
assertEquals(1, node.get("one").intValue());
assertFalse(node.has("two"));
assertEquals(3, node.get("three").intValue());
}
@Test
public void testFilterByFieldsWithNonPrimitive() throws IOException {
JsonNode node = readJson("filter-test-002");
JsonFilters.filterByFields(node, Lists.newArrayList("two"));
assertEquals("{\"two\":{\"three\":3,\"four\":4}}", objectMapper.writeValueAsString(node));
}
@Test
public void testFilterByFieldsWithDeepField() throws IOException {
JsonNode node = readJson("filter-test-002");
JsonFilters.filterByFields(node, Lists.newArrayList("two.three"));
assertEquals("{\"two\":{\"three\":3}}", objectMapper.writeValueAsString(node));
}
@Test
public void testFilterByFieldsWithVeryDeepField() throws IOException {
JsonNode node = readJson("filter-test-003");
JsonFilters.filterByFields(node, Lists.newArrayList("two.three.four.five"));
assertFalse(node.has("one"));
String json = objectMapper.writeValueAsString(node.get("two").get("three").get("four").get("five"));
assertEquals("{\"six\":6,\"seven\":7}", json);
}
@Test
public void testFilterByFieldsWithArray() throws IOException {
JsonNode node = readJson("filter-test-004");
JsonFilters.filterByFields(node, Lists.newArrayList("one.two"));
ArrayNode one = (ArrayNode) node.get("one");
assertEquals(one.size(), 2);
for (int i=0; i<one.size(); i++) {
JsonNode childOfOne = one.get(i);
assertFalse(childOfOne.has("three"));
assertEquals(2, childOfOne.get("two").intValue());
}
}
private JsonNode readJson(String name) throws IOException {
URL resource = Resources.getResource("sonia/scm/api/v2/" + name + ".json");
return objectMapper.readTree(resource);
}
}

View File

@@ -0,0 +1,122 @@
package sonia.scm.api.v2;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.web.JsonEnricher;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class JsonMarshallingResponseFilterTest {
@Mock
private ContainerRequestContext requestContext;
@Mock
private ContainerResponseContext responseContext;
@Mock
private UriInfo uriInfo;
@Captor
private ArgumentCaptor<JsonNode> jsonNodeCaptor;
private final ObjectMapper mapper = new ObjectMapper();
private Set<JsonEnricher> enrichers;
private JsonMarshallingResponseFilter filter;
@Before
public void setUpObjectUnderTest() throws URISyntaxException {
this.enrichers = new HashSet<>();
filter = new JsonMarshallingResponseFilter(mapper, enrichers);
when(requestContext.getUriInfo()).thenReturn(uriInfo);
when(uriInfo.getRequestUri()).thenReturn(new URI("https://www.scm-manager.org/scm/api/v2/repositories"));
}
@Test
public void testFilter() {
when(responseContext.hasEntity()).thenReturn(Boolean.TRUE);
when(responseContext.getEntity()).thenReturn(new JsonMarshallingResponseFilterTest.Sample("one-two-three"));
when(responseContext.getMediaType()).thenReturn(VndMediaType.jsonType("sample"));
filter.filter(requestContext, responseContext);
verify(responseContext).setEntity(jsonNodeCaptor.capture());
JsonNode node = jsonNodeCaptor.getValue();
assertEquals("one-two-three", node.get("value").asText());
}
@Test
public void testFilterWithEnricher() {
enrichers.add(context -> {
JsonNode node = context.getResponseEntity();
if (node.isObject()) {
((ObjectNode)node).put("version", 2);
}
});
when(responseContext.hasEntity()).thenReturn(Boolean.TRUE);
when(responseContext.getEntity()).thenReturn(new JsonMarshallingResponseFilterTest.Sample("one-two-three"));
when(responseContext.getMediaType()).thenReturn(VndMediaType.jsonType("sample"));
filter.filter(requestContext, responseContext);
verify(responseContext).setEntity(jsonNodeCaptor.capture());
JsonNode node = jsonNodeCaptor.getValue();
assertEquals(2, node.get("version").asInt());
}
@Test
public void testFilterWithoutEntity() {
filter.filter(requestContext, responseContext);
verify(responseContext, never()).setEntity(Mockito.anyObject());
}
@Test
public void testFilterWithNonVndEntity() {
when(responseContext.hasEntity()).thenReturn(Boolean.TRUE);
when(responseContext.getEntity()).thenReturn(new JsonMarshallingResponseFilterTest.Sample("one-two-three"));
when(responseContext.getMediaType()).thenReturn(MediaType.APPLICATION_JSON_TYPE);
filter.filter(requestContext, responseContext);
verify(responseContext, never()).setEntity(Mockito.anyObject());
}
public static class Sample {
private String value;
public Sample(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}

View File

@@ -35,37 +35,34 @@ package sonia.scm.it;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import sonia.scm.ScmState;
import sonia.scm.Type;
import sonia.scm.user.User;
import sonia.scm.util.IOUtil;
import static org.junit.Assert.*;
//~--- JDK imports ------------------------------------------------------------
import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.filter.LoggingFilter;
import com.sun.jersey.client.apache.ApacheHttpClient; import com.sun.jersey.client.apache.ApacheHttpClient;
import com.sun.jersey.client.apache.config.ApacheHttpClientConfig; import com.sun.jersey.client.apache.config.ApacheHttpClientConfig;
import com.sun.jersey.client.apache.config.DefaultApacheHttpClientConfig; import com.sun.jersey.client.apache.config.DefaultApacheHttpClientConfig;
import com.sun.jersey.core.util.MultivaluedMapImpl; import com.sun.jersey.core.util.MultivaluedMapImpl;
import sonia.scm.ScmState;
import sonia.scm.Type;
import sonia.scm.api.rest.JSONContextResolver;
import sonia.scm.api.rest.ObjectMapperProvider;
import sonia.scm.repository.Person;
import sonia.scm.repository.client.api.ClientCommand;
import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.user.User;
import sonia.scm.util.IOUtil;
import javax.ws.rs.core.MultivaluedMap;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.UUID; import java.util.UUID;
import javax.ws.rs.core.MultivaluedMap; import static org.junit.Assert.*;
import sonia.scm.api.rest.JSONContextResolver;
import sonia.scm.repository.Person; //~--- JDK imports ------------------------------------------------------------
import sonia.scm.repository.client.api.ClientCommand;
import sonia.scm.repository.client.api.RepositoryClient;
/** /**
* *
@@ -175,7 +172,7 @@ public final class IntegrationTestUtil
public static Client createClient() public static Client createClient()
{ {
DefaultApacheHttpClientConfig config = new DefaultApacheHttpClientConfig(); DefaultApacheHttpClientConfig config = new DefaultApacheHttpClientConfig();
config.getSingletons().add(new JSONContextResolver()); config.getSingletons().add(new JSONContextResolver(new ObjectMapperProvider().get()));
config.getProperties().put(ApacheHttpClientConfig.PROPERTY_HANDLE_COOKIES, true); config.getProperties().put(ApacheHttpClientConfig.PROPERTY_HANDLE_COOKIES, true);
return ApacheHttpClient.create(config); return ApacheHttpClient.create(config);

View File

@@ -0,0 +1,5 @@
{
"one": 1,
"two": 2,
"three": 3
}

View File

@@ -0,0 +1,8 @@
{
"one": 1,
"two": {
"three": 3,
"four": 4
},
"five": 5
}

View File

@@ -0,0 +1,13 @@
{
"one": 1,
"two": {
"three": {
"four": {
"five": {
"six": 6,
"seven": 7
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"one": [{
"two": 2
}, {
"two": 2,
"three": 3
}]
}