DS-3533 Add javadocs for new classes

This commit is contained in:
Chris Wilper
2019-10-31 15:59:31 -04:00
parent 71c81d28cf
commit 640c9071ef
5 changed files with 283 additions and 88 deletions

View File

@@ -17,11 +17,12 @@ import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import org.apache.log4j.Logger;
import org.dspace.app.rest.link.HalLinkFactory;
import org.dspace.app.rest.link.HalLinkService;
import org.dspace.app.rest.model.RestAddressableModel;
import org.dspace.app.rest.model.RestModel;
import org.dspace.app.rest.model.hateoas.DSpaceResource;
import org.dspace.app.rest.model.hateoas.HALResource;
import org.dspace.app.rest.projection.DefaultProjection;
import org.dspace.app.rest.projection.Projection;
import org.dspace.app.rest.utils.Utils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -30,11 +31,13 @@ import org.springframework.context.annotation.ClassPathScanningCandidateComponen
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.hateoas.Resource;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
/**
* Service to convert domain objects from the service layer to rest and resource form on the way out of DSpace.
* Converts domain objects from the DSpace service layer to rest objects, and from rest objects to resource
* objects, applying {@link Projection}s where applicable.
*/
@Component
@Service
public class ConverterService {
private static final Logger log = Logger.getLogger(ConverterService.class);
@@ -57,17 +60,205 @@ public class ConverterService {
@Autowired
private List<Projection> projections;
/**
* Converts the given model object to rest object, using the appropriate {@link DSpaceConverter}.
*
* @param modelObject the model object, which may be a JPA entity or other class provided by the DSpace
* service layer.
* @param <M> the type of model object. A converter {@link Component} must exist that takes this as input.
* @param <R> the inferred return type.
* @return the converted object. If it's a {@link RestAddressableModel}, its
* {@link RestAddressableModel#getProjection()} will be {@link DefaultProjection}.
* @throws IllegalArgumentException if there is no compatible converter.
* @throws ClassCastException if the converter's return type is not compatible with the inferred return type.
*/
public <M, R> R toRest(M modelObject) {
return toRest(modelObject, null);
}
/**
* Converts the given model object to a rest object, using the appropriate {@link DSpaceConverter}.
* <p>
* The projection's {@link Projection#transformModel(Object)} method will be automatically applied
* before conversion. If the rest object is a {@link RestModel}, the projection's
* {@link Projection#transformRest(RestModel)} method will be automatically called after conversion.
* If the rest object is a {@link RestAddressableModel}, its {@code projection} field will also be set so
* that a subsequent call to {@link #toResource(RestModel)} will respect the projection without it
* having to be provided as an argument again.
* </p>
*
* @param modelObject the model object, which may be a JPA entity or other class provided by the DSpace
* service layer.
* @param projectionName the name of the projection to use. If given as {@code null}, the default no-op
* projection, {@link DefaultProjection} will be used.
* @param <M> the type of model object. A converter {@link Component} must exist that takes this as input.
* @param <R> the inferred return type.
* @return the converted object. If it's a {@link RestAddressableModel}, its
* {@link RestAddressableModel#getProjection()} will be set to the named projection.
* @throws IllegalArgumentException if there is no compatible converter or no such projection.
* @throws ClassCastException if the converter's return type is not compatible with the inferred return type.
*/
public <M, R> R toRest(M modelObject, @Nullable String projectionName) {
Projection projection = projectionName == null ? Projection.DEFAULT : requireProjection(projectionName);
M transformedModel = projection.transformModel(modelObject);
DSpaceConverter<M, R> converter = requireConverter(modelObject.getClass());
R restObject = converter.convert(transformedModel);
if (restObject instanceof RestModel) {
if (restObject instanceof RestAddressableModel) {
RestAddressableModel ram = projection.transformRest((RestAddressableModel) restObject);
ram.setProjection(projection);
return (R) ram;
}
return (R) projection.transformRest((RestModel) restObject);
}
return restObject;
}
/**
* Gets the converter supporting the given class as input.
*
* @param sourceClass the desired converter's input type.
* @param <M> the converter's input type.
* @param <R> the converter's output type.
* @return the converter.
* @throws IllegalArgumentException if there is no such converter.
*/
<M, R> DSpaceConverter<M, R> getConverter(Class<M> sourceClass) {
return (DSpaceConverter<M, R>) requireConverter(sourceClass);
}
/**
* Converts the given rest object to a {@link HALResource} object, assigning the named projection beforehand,
* if it is a {@link RestAddressableModel}.
* <p>
* After the projection is assigned, behavior of this method is exactly the same as {@link #toResource(RestModel)}.
* </p>
*
* @param restObject the input rest object.
* @param projectionName the name of the projection to assign and use.
* @param <T> the return type, a subclass of {@link HALResource}.
* @return the fully converted resource, with all automatic links and embeds applied.
* @throws IllegalArgumentException if there is no such projection.
*/
public <T extends HALResource> T toResource(RestModel restObject, String projectionName) {
if (restObject instanceof RestAddressableModel) {
((RestAddressableModel) restObject).setProjection(requireProjection(projectionName));
}
return toResource(restObject);
}
/**
* Converts the given rest object to a {@link HALResource} object.
* <p>
* If the rest object is a {@link RestAddressableModel}, the projection returned by
* {@link RestAddressableModel#getProjection()} will be used to determine which optional
* embeds and links will be added, and {@link Projection#transformResource(HALResource)}
* will be automatically called before returning the final, fully converted resource.
* </p><p>
* In all cases, the {@link HalLinkService} will be used immediately after the resource is constructed,
* to ensure all {@link HalLinkFactory}s have had a chance to add links as needed.
* </p>
*
* @param restObject the input rest object.
* @param <T> the return type, a subclass of {@link HALResource}.
* @return the fully converted resource, with all automatic links and embeds applied.
* @throws IllegalArgumentException if there is no such projection.
*/
public <T extends HALResource> T toResource(RestModel restObject) {
T halResource = getResource(restObject);
if (restObject instanceof RestAddressableModel) {
utils.embedOrLinkClassLevelRels(halResource);
halLinkService.addLinks(halResource);
Projection projection = ((RestAddressableModel) restObject).getProjection();
return projection.transformResource(halResource);
} else {
halLinkService.addLinks(halResource);
}
return halResource;
}
/**
* Creates and returns an instance of the appropriate {@link HALResource} subclass for the given rest object.
* <p>
* <b>Note:</b> Only two forms of constructor are supported for resources that can be created with this method:
* A one-argument constructor taking the wrapped {@link RestModel}, and a two-argument constructor also taking
* a {@link Utils} instance. If both are found in a candidate resource's constructor, the two-argument form
* will be used.
* </p>
*
* @param restObject the rest object to wrap.
* @param <T> the return type, a subclass of {@link HALResource}.
* @return a new resource instance of the appropriate type.
*/
private <T extends HALResource> T getResource(RestModel restObject) {
Constructor constructor = resourceConstructors.get(restObject.getClass());
try {
if (constructor.getParameterCount() == 2) {
return (T) constructor.newInstance(restObject, utils);
} else {
return (T) constructor.newInstance(restObject);
}
} catch (InvocationTargetException e) {
if (e.getTargetException() instanceof RuntimeException) {
throw (RuntimeException) e.getTargetException();
}
throw new RuntimeException(e);
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* Gets the projection with the given name or throws an {@link IllegalArgumentException}.
*
* @param projectionName the projection name.
* @return the projection.
* @throws IllegalArgumentException if not found.
*/
private Projection requireProjection(String projectionName) {
if (!projectionMap.containsKey(projectionName)) {
throw new IllegalArgumentException("No such projection: " + projectionName);
}
return projectionMap.get(projectionName);
}
/**
* Gets the converter that supports the given source/input class or throws an {@link IllegalArgumentException}.
*
* @param sourceClass the source/input class.
* @return the converter.
* @throws IllegalArgumentException if not found.
*/
private DSpaceConverter requireConverter(Class sourceClass) {
if (converterMap.containsKey(sourceClass)) {
return converterMap.get(sourceClass);
}
for (Class converterSourceClass : converterMap.keySet()) {
if (converterSourceClass.isAssignableFrom(sourceClass)) {
return converterMap.get(converterSourceClass);
}
}
throw new IllegalArgumentException("No converter found to get rest class from " + sourceClass);
}
/**
* Populates maps of injected components and constructors to be used by this service's public methods.
*/
@PostConstruct
private void initialize() {
// put all available projections in a map keyed by name
for (Projection projection : projections) {
projectionMap.put(projection.getName(), projection);
}
projectionMap.put(Projection.DEFAULT.getName(), Projection.DEFAULT);
// put all available converters in a map keyed by model (input) class
for (DSpaceConverter converter : converters) {
converterMap.put(converter.getModelClass(), converter);
}
// scan all resource classes and look for compatible rest classes (by naming convention),
// creating a map of resource constructors keyed by rest class, for later use.
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AssignableTypeFilter(Resource.class));
Set<BeanDefinition> beanDefinitions = provider.findCandidateComponents(
@@ -96,94 +287,13 @@ public class ConverterService {
if (compatibleConstructor != null) {
resourceConstructors.put(restClass, compatibleConstructor);
} else {
logSkipping(resourceClassName, "compatible constructor not found");
log.warn("Skipping registration of resource class " + resourceClassName
+ "; compatible constructor not found");
}
} catch (ClassNotFoundException e) {
logSkipping(resourceClassName, "rest class not found: " + restClassName);
log.warn("Skipping registration of resource class " + resourceClassName
+ "; rest class not found: " + restClassName);
}
}
}
private void logSkipping(String resourceClassName, String reason) {
log.warn("Skipping registration of resource class " + resourceClassName + "; " + reason);
}
private Projection requireProjection(String projectionName) {
if (!projectionMap.containsKey(projectionName)) {
throw new IllegalArgumentException("No such projection: " + projectionName);
}
return projectionMap.get(projectionName);
}
public <T> T toRest(Object modelObject) {
return toRest(modelObject, null);
}
public <M, R> R toRest(M modelObject, @Nullable String projectionName) {
Projection projection = projectionName == null ? Projection.DEFAULT : requireProjection(projectionName);
M transformedModel = projection.transformModel(modelObject);
DSpaceConverter<M, R> converter = requireConverter(modelObject.getClass());
R restObject = converter.convert(transformedModel);
if (restObject instanceof RestAddressableModel) {
RestAddressableModel ram = projection.transformRest((RestAddressableModel) restObject);
ram.setProjection(projection);
return (R) ram;
}
return restObject;
}
public <M, R> DSpaceConverter<M, R> getConverter(Class<M> sourceClass) {
return (DSpaceConverter<M, R>) requireConverter(sourceClass);
}
private DSpaceConverter requireConverter(Class sourceClass) {
if (converterMap.containsKey(sourceClass)) {
return converterMap.get(sourceClass);
}
for (Class converterSourceClass : converterMap.keySet()) {
if (converterSourceClass.isAssignableFrom(sourceClass)) {
return converterMap.get(converterSourceClass);
}
}
throw new IllegalArgumentException("No converter found to get rest class from " + sourceClass);
}
public <T extends HALResource> T toResource(RestModel restObject, @Nullable String projectionName) {
if (restObject instanceof RestAddressableModel) {
((RestAddressableModel) restObject).setProjection(requireProjection(projectionName));
}
return toResource(restObject);
}
public <T extends HALResource> T toResource(RestModel restObject) {
T halResource = getResource(restObject);
if (restObject instanceof RestAddressableModel) {
utils.embedOrLinkClassLevelRels(halResource);
halLinkService.addLinks(halResource);
Projection projection = ((RestAddressableModel) restObject).getProjection();
return projection.transformResource(halResource);
} else {
halLinkService.addLinks(halResource);
}
return halResource;
}
private <T extends HALResource> T getResource(RestModel restObject) {
Constructor constructor = resourceConstructors.get(restObject.getClass());
try {
if (constructor == null) {
constructor = DSpaceResource.class.getDeclaredConstructor();
}
if (constructor.getParameterCount() == 2) {
return (T) constructor.newInstance(restObject, utils);
} else {
return (T) constructor.newInstance(restObject);
}
} catch (InstantiationException
| IllegalAccessException
| InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -11,6 +11,9 @@ import org.dspace.app.rest.model.LinkRest;
import org.dspace.app.rest.model.RestModel;
import org.dspace.app.rest.model.hateoas.HALResource;
/**
* Abstract base class for projections. By default each method has no effect unless overridden by a subclass.
*/
public abstract class AbstractProjection implements Projection {
@Override

View File

@@ -7,6 +7,9 @@
*/
package org.dspace.app.rest.projection;
/**
* The default projection, which has no effect.
*/
public class DefaultProjection extends AbstractProjection {
public final static String NAME = "default";

View File

@@ -11,6 +11,9 @@ import org.dspace.app.rest.model.LinkRest;
import org.dspace.app.rest.model.hateoas.HALResource;
import org.springframework.stereotype.Component;
/**
* A projection that provides an abbreviated form of any resource that omits all optional embeds.
*/
@Component
public class ListProjection extends AbstractProjection {

View File

@@ -7,10 +7,56 @@
*/
package org.dspace.app.rest.projection;
import javax.persistence.Entity;
import org.dspace.app.rest.model.LinkRest;
import org.dspace.app.rest.model.RestModel;
import org.dspace.app.rest.model.hateoas.HALResource;
import org.dspace.app.rest.repository.DSpaceRestRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RestController;
/**
* A pluggable, uniquely-named {@link Component} that provides a way to change how a domain object is represented,
* at one or more points in its lifecycle on the way to its being exposed via the REST API.
*
* <h2>The object lifecycle</h2>
*
* <p>
* While fulfilling a typical REST request, a DSpace domain object takes three major forms, in order:
* </p>
*
* <ul>
* <li> A model object provided by some service. This is typically a JPA {@link Entity}.</li>
* <li> A {@link RestModel} object provided by a {@link DSpaceRestRepository}.</li>
* <li> A {@link HALResource} object provided by the {@link RestController} to Spring, which then
* serializes it as JSON for the client to consume.</li>
* </ul>
*
* <h2>What a projection can modify, and when</h2>
*
* A {@code Projection} implementation is capable of adding to or omitting information from the object
* in any of these forms, at the following points in time:
*
* <ul>
* <li> Before it is converted to a {@link RestModel}, the projection may modify it
* via {@link #transformModel(Object)}.</li>
* <li> After it is converted to a {@link RestModel}, the projection may modify it
* via {@link #transformRest(RestModel)}.</li>
* <li> During conversion to a {@link HALResource}, the projection may opt out of certain annotation-discovered
* HAL embeds and links via {@link #allowOptionalEmbed(HALResource, LinkRest)}
* and {@link #allowOptionalLink(HALResource, LinkRest)}.</li>
* <li> After conversion to a {@link HALResource}, the projection may modify it
* via {@link #transformResource(HALResource)}.</li>
* </ul>
*
* <h2>How a projection is chosen</h2>
*
* When a REST request is made, the use of a projection may be explicit, as when it is provided as an argument
* to the request, e.g. {@code /items/{uuid}?projection={projectionName}}. It may also be implicit, as when the
* {@link ListProjection} is used automatically, in order to provide an abbreviated representation when serving
* a collection of resources.
*/
public interface Projection {
/**
@@ -25,10 +71,40 @@ public interface Projection {
*/
String getName();
/**
* Transforms the original model object (e.g. JPA entity) before conversion to a {@link RestModel}.
*
* This is a good place to omit data for certain properties that should not be included in the object's
* representation as a {@link HALResource}. Omitting these properties early helps to prevent unnecessary
* database calls for lazy-loaded properties that are unwanted for the projection.
*
* @param modelObject the original model object, which may be of any type.
* @param <T> the return type, which must be the same type as the given model object.
* @return the transformed model object, or the original, if the projection does not modify it.
*/
<T> T transformModel(T modelObject);
/**
* Transforms the rest object after it was converted from a model object.
*
* This may add data to, or omit data from the rest representation of the object.
*
* @param restObject the rest object.
* @param <T> the return type, which must be of the same type as the given rest object.
* @return the transformed rest object, or the original, if the projection does not modify it.
*/
<T extends RestModel> T transformRest(T restObject);
/**
* Transforms the resource object after it has been constructed and any constructor or annotation-based
* links and embeds have been added.
*
* This may add data to, or omit data from the HAL resource representation of the object.
*
* @param halResource the resource object.
* @param <T> the return type, which must be of the same type as the given resource object.
* @return the transformed resource object, or the original, if the projection does not modify it.
*/
<T extends HALResource> T transformResource(T halResource);
/**