I have a simple UserRepository
which exposed using Spring Data REST
.
Here is the User
entity class:
@Document(collecti
I was able to create a ResourceProcessor
class which applies projections on any resource as suggested in DATAREST-428. It works the following way: if projection parameter is specified in URL - the specified projection will be applied, if not - projection with name default will be returned, applied first found projection will be applied. Also, I had to add custom ProjectingResource
which ignores the links, otherwise there are two _links
keys in the returning JSON.
/**
* Projecting resource used for {@link ProjectingProcessor}. Does not include empty links in JSON, otherwise two
* _links keys are present in returning JSON.
*
* @param <T>
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
class ProjectingResource<T> extends Resource<T> {
ProjectingResource(final T content) {
super(content);
}
}
/**
* Resource processor for all resources which applies projection for single resource. By default, projections
* are not
* applied when working with single resource, e.g. http://127.0.0.1:8080/users/580793f642d54436e921f6ca. See
* related issue <a href="https://jira.spring.io/browse/DATAREST-428">DATAREST-428</a>
*/
@Component
public class ProjectingProcessor implements ResourceProcessor<Resource<Object>> {
private static final String PROJECTION_PARAMETER = "projection";
private final ProjectionFactory projectionFactory;
private final RepositoryRestConfiguration repositoryRestConfiguration;
private final HttpServletRequest request;
public ProjectingProcessor(@Autowired final RepositoryRestConfiguration repositoryRestConfiguration,
@Autowired final ProjectionFactory projectionFactory,
@Autowired final HttpServletRequest request) {
this.repositoryRestConfiguration = repositoryRestConfiguration;
this.projectionFactory = projectionFactory;
this.request = request;
}
@Override
public Resource<Object> process(final Resource<Object> resource) {
if (AopUtils.isAopProxy(resource.getContent())) {
return resource;
}
final Optional<Class<?>> projectionType = findProjectionType(resource.getContent());
if (projectionType.isPresent()) {
final Object projection = projectionFactory.createProjection(projectionType.get(), resource
.getContent());
return new ProjectingResource<>(projection);
}
return resource;
}
private Optional<Class<?>> findProjectionType(final Object content) {
final String projectionParameter = request.getParameter(PROJECTION_PARAMETER);
final Map<String, Class<?>> projectionsForType = repositoryRestConfiguration.getProjectionConfiguration()
.getProjectionsFor(content.getClass());
if (!projectionsForType.isEmpty()) {
if (!StringUtils.isEmpty(projectionParameter)) {
// projection parameter specified
final Class<?> projectionClass = projectionsForType.get(projectionParameter);
if (projectionClass != null) {
return Optional.of(projectionClass);
}
} else if (projectionsForType.containsKey(ProjectionName.DEFAULT)) {
// default projection exists
return Optional.of(projectionsForType.get(ProjectionName.DEFAULT));
}
// no projection parameter specified
return Optional.of(projectionsForType.values().iterator().next());
}
return Optional.empty();
}
}
I was looking at something similar recently and ended up going round in circles when trying to approach it from the Spring Data /Jackson side of things.
An alternative, and very simple solution, then is to approach it from a different angle and ensure the Projection parameter in the HTTP request is always present. This can be done by using a Servlet Filter to modify the parameters of the incoming request.
This would look something like the below:
public class ProjectionResolverFilter extends GenericFilterBean {
private static final String REQUEST_PARAM_PROJECTION_KEY = "projection";
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
if (shouldApply(request)) {
chain.doFilter(new ResourceRequestWrapper(request), res);
} else {
chain.doFilter(req, res);
}
}
/**
*
* @param request
* @return True if this filter should be applied for this request, otherwise
* false.
*/
protected boolean shouldApply(HttpServletRequest request) {
return request.getServletPath().matches("some-path");
}
/**
* HttpServletRequestWrapper implementation which allows us to wrap and
* modify the incoming request.
*
*/
public class ResourceRequestWrapper extends HttpServletRequestWrapper {
public ResourceRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(final String name) {
if (name.equals(REQUEST_PARAM_PROJECTION_KEY)) {
return "nameOfDefaultProjection";
}
return super.getParameter(name);
}
}
}