diff --git a/pom.xml b/pom.xml
index 8a6874fd9a..636418bb69 100644
--- a/pom.xml
+++ b/pom.xml
@@ -484,6 +484,7 @@
2.0.1
1.19.4
+ 2.8.6
4.0
diff --git a/scm-core/pom.xml b/scm-core/pom.xml
index 5dfba770e6..56901d6d55 100644
--- a/scm-core/pom.xml
+++ b/scm-core/pom.xml
@@ -82,7 +82,17 @@
javax.ws.rs-api
${jaxrs.version}
-
+
+ com.fasterxml.jackson.core
+ jackson-core
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
diff --git a/scm-core/src/main/java/sonia/scm/web/JsonEnricher.java b/scm-core/src/main/java/sonia/scm/web/JsonEnricher.java
new file mode 100644
index 0000000000..9b0f078f8b
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/web/JsonEnricher.java
@@ -0,0 +1,10 @@
+package sonia.scm.web;
+
+import sonia.scm.plugin.ExtensionPoint;
+
+@ExtensionPoint
+public interface JsonEnricher {
+
+ void enrich(JsonEnricherContext context);
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/web/JsonEnricherContext.java b/scm-core/src/main/java/sonia/scm/web/JsonEnricherContext.java
new file mode 100644
index 0000000000..6dd88d326c
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/web/JsonEnricherContext.java
@@ -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;
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java
new file mode 100644
index 0000000000..92b85b022f
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java
@@ -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);
+ }
+}
diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml
index de837dd322..4a01375ef4 100644
--- a/scm-webapp/pom.xml
+++ b/scm-webapp/pom.xml
@@ -560,7 +560,6 @@
1.0
0.8.17
3.1.3.Final
- 2.8.6
Tomcat
e1
javascript:S3827
diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java
index 8d89f2b2eb..2f6e7341bb 100644
--- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java
+++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java
@@ -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);
diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/JSONContextResolver.java b/scm-webapp/src/main/java/sonia/scm/api/rest/JSONContextResolver.java
index 8a72911bc9..1dc1090604 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/rest/JSONContextResolver.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/rest/JSONContextResolver.java
@@ -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
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;
}
-
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/ObjectMapperProvider.java b/scm-webapp/src/main/java/sonia/scm/api/rest/ObjectMapperProvider.java
new file mode 100644
index 0000000000..68522234d3
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/api/rest/ObjectMapperProvider.java
@@ -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 {
+
+ @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()
+ );
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/FieldContainerResponseFilter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/FieldContainerResponseFilter.java
new file mode 100644
index 0000000000..4ba93a31d5
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/FieldContainerResponseFilter.java
@@ -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 entity = getJsonEntity(responseContext);
+ if (entity.isPresent()) {
+ List fields = extractFieldsFrom(requestContext);
+ if (!fields.isEmpty()) {
+ JsonFilters.filterByFields(entity.get(), fields);
+ }
+ }
+ }
+
+ private Optional 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 extractFieldsFrom(ContainerRequestContext requestContext) {
+ List fields = Lists.newArrayList();
+
+ List fieldParameters = getFieldParameterFrom(requestContext);
+ if (fieldParameters != null && !fieldParameters.isEmpty()) {
+ for (String fieldParameter : fieldParameters) {
+ appendFieldsFromParameter(fields, fieldParameter);
+ }
+ }
+
+ return fields;
+ }
+
+ private List getFieldParameterFrom(ContainerRequestContext requestContext) {
+ MultivaluedMap queryParameters = requestContext.getUriInfo().getQueryParameters();
+ return queryParameters.get(PARAMETER_FIELDS);
+ }
+
+ private void appendFieldsFromParameter(List fields, String fieldParameter) {
+ for (String field : fieldParameter.split(FIELD_SEPARATOR)) {
+ fields.add(field);
+ }
+ }
+
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/JsonFilters.java b/scm-webapp/src/main/java/sonia/scm/api/v2/JsonFilters.java
new file mode 100644
index 0000000000..a06e3118b7
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/JsonFilters.java
@@ -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 fields) {
+ filterNode(createJsonFilterNode(fields), root);
+ }
+
+ private static JsonFilterNode createJsonFilterNode(Iterable 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> entryIterator = objectNode.fields();
+ while (entryIterator.hasNext()) {
+ Map.Entry 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 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();
+ }
+ }
+
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/JsonMarshallingResponseFilter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/JsonMarshallingResponseFilter.java
new file mode 100644
index 0000000000..3be159dbf8
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/JsonMarshallingResponseFilter.java
@@ -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 enrichers;
+
+ @Inject
+ public JsonMarshallingResponseFilter(ObjectMapper objectMapper, Set 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());
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmMediaType.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmMediaType.java
index 81310dd4df..d9d8f0ecf1 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmMediaType.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmMediaType.java
@@ -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;
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java
index f3462eadda..f26c90b957 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java
@@ -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 {
public static final int DEFAULT_PAGE_SIZE = 10;
private final UserDto2UserMapper dtoToUserMapper;
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserSubResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserSubResource.java
index 46789b2deb..3f5a1752b5 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserSubResource.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserSubResource.java
@@ -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 {
private final UserDto2UserMapper dtoToUserMapper;
private final User2UserDtoMapper userToDtoMapper;
diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/JSONContextResolverTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/JSONContextResolverTest.java
index dabf75b2b0..8e3b39edc7 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/rest/JSONContextResolverTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/rest/JSONContextResolverTest.java
@@ -59,7 +59,7 @@ import static org.junit.Assert.*;
*/
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.
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/FieldContainerResponseFilterTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/FieldContainerResponseFilterTest.java
new file mode 100644
index 0000000000..3855c380b2
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/FieldContainerResponseFilterTest.java
@@ -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 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);
+ }
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/JsonFiltersTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/JsonFiltersTest.java
new file mode 100644
index 0000000000..bfa280d821
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/JsonFiltersTest.java
@@ -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 jsonNodeCaptor;
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ private Set 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;
+ }
+ }
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java b/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java
index dbafbb44f7..e12057e47a 100644
--- a/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java
+++ b/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java
@@ -35,37 +35,34 @@ package sonia.scm.it;
//~--- 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.ClientResponse;
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.config.ApacheHttpClientConfig;
import com.sun.jersey.client.apache.config.DefaultApacheHttpClientConfig;
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.FileOutputStream;
import java.io.IOException;
-
import java.util.ArrayList;
import java.util.Collection;
import java.util.UUID;
-import javax.ws.rs.core.MultivaluedMap;
-import sonia.scm.api.rest.JSONContextResolver;
-import sonia.scm.repository.Person;
-import sonia.scm.repository.client.api.ClientCommand;
-import sonia.scm.repository.client.api.RepositoryClient;
+import static org.junit.Assert.*;
+
+//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -175,7 +172,7 @@ public final class IntegrationTestUtil
public static Client createClient()
{
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);
return ApacheHttpClient.create(config);
diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/filter-test-001.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/filter-test-001.json
new file mode 100644
index 0000000000..120cbb2840
--- /dev/null
+++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/filter-test-001.json
@@ -0,0 +1,5 @@
+{
+ "one": 1,
+ "two": 2,
+ "three": 3
+}
diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/filter-test-002.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/filter-test-002.json
new file mode 100644
index 0000000000..489d211a17
--- /dev/null
+++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/filter-test-002.json
@@ -0,0 +1,8 @@
+{
+ "one": 1,
+ "two": {
+ "three": 3,
+ "four": 4
+ },
+ "five": 5
+}
diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/filter-test-003.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/filter-test-003.json
new file mode 100644
index 0000000000..8262837427
--- /dev/null
+++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/filter-test-003.json
@@ -0,0 +1,13 @@
+{
+ "one": 1,
+ "two": {
+ "three": {
+ "four": {
+ "five": {
+ "six": 6,
+ "seven": 7
+ }
+ }
+ }
+ }
+}
diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/filter-test-004.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/filter-test-004.json
new file mode 100644
index 0000000000..2a8cb40db7
--- /dev/null
+++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/filter-test-004.json
@@ -0,0 +1,8 @@
+{
+ "one": [{
+ "two": 2
+ }, {
+ "two": 2,
+ "three": 3
+ }]
+}