/* * Copyright 2014-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.rest.webmvc.alps; import static org.springframework.hateoas.alps.Alps.*; import lombok.NonNull; import lombok.RequiredArgsConstructor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.springframework.context.NoSuchMessageException; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.SimpleAssociationHandler; import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.annotation.Description; import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.AnnotationBasedResourceDescription; import org.springframework.data.rest.core.mapping.MethodResourceMapping; import org.springframework.data.rest.core.mapping.ParameterMetadata; import org.springframework.data.rest.core.mapping.ResourceDescription; import org.springframework.data.rest.core.mapping.ResourceMapping; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.mapping.ResourceType; import org.springframework.data.rest.core.mapping.SimpleResourceDescription; import org.springframework.data.rest.core.mapping.SupportedHttpMethods; import org.springframework.data.rest.webmvc.ProfileController; import org.springframework.data.rest.webmvc.RootResourceInformation; import org.springframework.data.rest.webmvc.json.EnumTranslator; import org.springframework.data.rest.webmvc.json.JacksonMetadata; import org.springframework.data.rest.webmvc.mapping.Associations; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.Link; import org.springframework.hateoas.TemplateVariable; import org.springframework.hateoas.alps.Alps; import org.springframework.hateoas.alps.Descriptor; import org.springframework.hateoas.alps.Descriptor.DescriptorBuilder; import org.springframework.hateoas.alps.Doc; import org.springframework.hateoas.alps.Format; import org.springframework.hateoas.alps.Type; import org.springframework.http.HttpMethod; import org.springframework.util.StringUtils; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; /** * Converter to create Alps {@link Descriptor} instances for a {@link RootResourceInformation}. * * @author Oliver Gierke * @author Greg Turnquist */ @RequiredArgsConstructor public class RootResourceInformationToAlpsDescriptorConverter { private static final List<HttpMethod> UNDOCUMENTED_METHODS = Arrays.asList(HttpMethod.OPTIONS, HttpMethod.HEAD); private final @NonNull Associations associations; private final @NonNull Repositories repositories; private final @NonNull PersistentEntities persistentEntities; private final @NonNull EntityLinks entityLinks; private final @NonNull MessageSourceAccessor messageSource; private final @NonNull RepositoryRestConfiguration configuration; private final @NonNull ObjectMapper mapper; private final @NonNull EnumTranslator translator; /* * (non-Javadoc) * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) */ public Alps convert(RootResourceInformation resourceInformation) { Class<?> type = resourceInformation.getDomainType(); List<Descriptor> descriptors = new ArrayList<Descriptor>(); Descriptor representationDescriptor = buildRepresentationDescriptor(type); descriptors.add(representationDescriptor); SupportedHttpMethods supportedHttpMethods = resourceInformation.getSupportedMethods(); for (HttpMethod method : supportedHttpMethods.getMethodsFor(ResourceType.COLLECTION)) { if (!UNDOCUMENTED_METHODS.contains(method)) { descriptors.add(buildCollectionResourceDescriptor(type, resourceInformation, representationDescriptor, method)); } } for (HttpMethod method : supportedHttpMethods.getMethodsFor(ResourceType.ITEM)) { if (!UNDOCUMENTED_METHODS.contains(method)) { descriptors.add(buildItemResourceDescriptor(resourceInformation, representationDescriptor, method)); } } descriptors.addAll(buildSearchResourceDescriptors(resourceInformation.getPersistentEntity())); return Alps.alps().descriptors(descriptors).build(); } private Descriptor buildRepresentationDescriptor(Class<?> type) { ResourceMetadata metadata = associations.getMetadataFor(type); String href = ProfileController.getPath(this.configuration, metadata); return descriptor().// id(getRepresentationDescriptorId(metadata)).// href(href).// doc(getDocFor(metadata.getItemResourceDescription())).// descriptors(buildPropertyDescriptors(type, metadata.getItemResourceRel())).// build(); } private Descriptor buildCollectionResourceDescriptor(Class<?> type, RootResourceInformation resourceInformation, Descriptor representationDescriptor, HttpMethod method) { ResourceMetadata metadata = associations.getMetadataFor(type); List<Descriptor> nestedDescriptors = new ArrayList<Descriptor>(); nestedDescriptors.addAll(getPaginationDescriptors(type, method)); nestedDescriptors.addAll(getProjectionDescriptor(type, method)); Type descriptorType = getType(method); return descriptor().// id(prefix(method).concat(metadata.getRel())).// name(metadata.getRel()).// type(descriptorType).// doc(getDocFor(metadata.getDescription())).// rt("#" + representationDescriptor.getId()).// descriptors(nestedDescriptors).build(); } /** * Builds a descriptor for the projection parameter of the given resource. * * @param metadata * @return */ private Descriptor buildProjectionDescriptor(ResourceMetadata metadata) { ProjectionDefinitionConfiguration projectionConfiguration = configuration.getProjectionConfiguration(); String projectionParameterName = projectionConfiguration.getParameterName(); Map<String, Class<?>> projections = projectionConfiguration.getProjectionsFor(metadata.getDomainType()); List<Descriptor> projectionDescriptors = new ArrayList<Descriptor>(projections.size()); for (Entry<String, Class<?>> projection : projections.entrySet()) { Class<?> type = projection.getValue(); String key = String.format("%s.%s.%s", metadata.getRel(), projectionParameterName, projection.getKey()); ResourceDescription fallback = SimpleResourceDescription.defaultFor(key); AnnotationBasedResourceDescription projectionDescription = new AnnotationBasedResourceDescription(type, fallback); projectionDescriptors.add(// descriptor().// type(Type.SEMANTIC).// name(projection.getKey()).// doc(getDocFor(projectionDescription)).// descriptors(createJacksonDescriptor(projection.getKey(), type)).// build()); } return descriptor().// type(Type.SEMANTIC).// name(projectionParameterName).// doc(getDocFor(SimpleResourceDescription.defaultFor(projectionParameterName))).// descriptors(projectionDescriptors).build(); } private List<Descriptor> createJacksonDescriptor(String name, Class<?> type) { List<Descriptor> descriptors = new ArrayList<Descriptor>(); for (BeanPropertyDefinition definition : new JacksonMetadata(mapper, type)) { AnnotatedMethod getter = definition.getGetter(); Description description = getter.getAnnotation(Description.class); ResourceDescription fallback = SimpleResourceDescription .defaultFor(String.format("%s.%s", name, definition.getName())); ResourceDescription resourceDescription = description == null ? null : new AnnotationBasedResourceDescription(description, fallback); descriptors.add(// descriptor().// name(definition.getName()).// type(Type.SEMANTIC).// doc(getDocFor(resourceDescription)).// build()); } return descriptors; } private Descriptor buildItemResourceDescriptor(RootResourceInformation resourceInformation, Descriptor representationDescriptor, HttpMethod method) { PersistentEntity<?, ?> entity = resourceInformation.getPersistentEntity(); ResourceMetadata metadata = associations.getMetadataFor(entity.getType()); return descriptor().// id(prefix(method).concat(metadata.getItemResourceRel())).// name(metadata.getItemResourceRel()).// type(getType(method)).// doc(getDocFor(metadata.getItemResourceDescription())).// rt("#".concat(representationDescriptor.getId())). // descriptors(getProjectionDescriptor(entity.getType(), method)).// build(); } private List<Descriptor> getProjectionDescriptor(Class<?> type, HttpMethod method) { if (!Type.SAFE.equals(getType(method))) { return Collections.emptyList(); } ProjectionDefinitionConfiguration projectionConfiguration = configuration.getProjectionConfiguration(); return projectionConfiguration.hasProjectionFor(type) ? Arrays.asList(buildProjectionDescriptor(associations.getMetadataFor(type))) : Collections.<Descriptor> emptyList(); } /** * Creates the {@link Descriptor}s for pagination parameters. * * @param type * @return */ private List<Descriptor> getPaginationDescriptors(Class<?> type, HttpMethod method) { RepositoryInformation information = repositories.getRequiredRepositoryInformation(type); if (!information.isPagingRepository() || !getType(method).equals(Type.SAFE)) { return Collections.emptyList(); } Link linkToCollectionResource = entityLinks.linkToCollectionResource(type); List<TemplateVariable> variables = linkToCollectionResource.getVariables(); List<Descriptor> descriptors = new ArrayList<Descriptor>(variables.size()); ProjectionDefinitionConfiguration projectionConfiguration = configuration.getProjectionConfiguration(); for (TemplateVariable variable : variables) { // Skip projection parameter if (projectionConfiguration.getParameterName().equals(variable.getName())) { continue; } ResourceDescription description = SimpleResourceDescription.defaultFor(variable.getDescription()); descriptors.add(// descriptor().// name(variable.getName()).// type(Type.SEMANTIC).// doc(getDocFor(description)).// build()); } return descriptors; } private List<Descriptor> buildPropertyDescriptors(final Class<?> type, String baseRel) { final PersistentEntity<?, ?> entity = persistentEntities.getRequiredPersistentEntity(type); final List<Descriptor> propertyDescriptors = new ArrayList<Descriptor>(); final JacksonMetadata jackson = new JacksonMetadata(mapper, type); final ResourceMetadata metadata = associations.getMetadataFor(entity.getType()); entity.doWithProperties(new SimplePropertyHandler() { @Override public void doWithPersistentProperty(PersistentProperty<?> property) { BeanPropertyDefinition propertyDefinition = jackson.getDefinitionFor(property); ResourceMapping propertyMapping = metadata.getMappingFor(property); if (propertyDefinition != null) { if (property.isIdProperty() && !configuration.isIdExposedFor(property.getOwner().getType())) { return; } propertyDescriptors.add(// descriptor(). // type(Type.SEMANTIC).// name(propertyDefinition.getName()).// doc(getDocFor(propertyMapping.getDescription(), property)).// build()); } } }); entity.doWithAssociations(new SimpleAssociationHandler() { @Override public void doWithAssociation(Association<? extends PersistentProperty<?>> association) { PersistentProperty<?> property = association.getInverse(); if (!jackson.isExported(property) || !associations.isLinkableAssociation(property)) { return; } ResourceMapping mapping = metadata.getMappingFor(property); DescriptorBuilder builder = descriptor().// name(mapping.getRel()).doc(getDocFor(mapping.getDescription())); ResourceMetadata targetTypeMetadata = associations.getMetadataFor(property.getActualType()); String href = ProfileController.getPath(configuration, targetTypeMetadata) + "#" + getRepresentationDescriptorId(targetTypeMetadata); Link link = new Link(href).withSelfRel(); builder.// type(Type.SAFE).// rt(link.getHref()); propertyDescriptors.add(builder.build()); } }); return propertyDescriptors; } private Collection<Descriptor> buildSearchResourceDescriptors(PersistentEntity<?, ?> entity) { ResourceMetadata metadata = associations.getMetadataFor(entity.getType()); List<Descriptor> descriptors = new ArrayList<Descriptor>(); for (MethodResourceMapping methodMapping : metadata.getSearchResourceMappings()) { List<Descriptor> parameterDescriptors = new ArrayList<Descriptor>(); for (ParameterMetadata parameterMetadata : methodMapping.getParametersMetadata()) { parameterDescriptors.add(// descriptor().// name(parameterMetadata.getName()).// doc(getDocFor(parameterMetadata.getDescription())).// type(Type.SEMANTIC)// .build()); } descriptors.add(descriptor().// type(Type.SAFE).// name(methodMapping.getRel()).// descriptors(parameterDescriptors).// build()); } return descriptors; } private Doc getDocFor(ResourceDescription description) { return getDocFor(description, null); } @SuppressWarnings("unchecked") private Doc getDocFor(ResourceDescription description, PersistentProperty<?> property) { if (description == null) { return null; } String message = resolveMessage(description); // Manually post process the default message for enumerations if needed if (configuration.isEnableEnumTranslation() && property != null && property.getType().isEnum()) { if (description.isDefault()) { return new Doc(StringUtils.collectionToDelimitedString( translator.getValues((Class<? extends Enum<?>>) property.getType()), ", "), Format.TEXT); } } return message == null ? null : new Doc(message, Format.TEXT); } private String resolveMessage(ResourceDescription description) { if (!description.isDefault()) { return description.getMessage(); } try { return messageSource.getMessage(description); } catch (NoSuchMessageException o_O) { return configuration.getMetadataConfiguration().omitUnresolvableDescriptionKeys() ? null : description.getMessage(); } } private static String getRepresentationDescriptorId(ResourceMetadata metadata) { return metadata.getItemResourceRel().concat("-representation"); } private static String prefix(HttpMethod method) { switch (method) { case GET: return "get-"; case POST: return "create-"; case DELETE: return "delete-"; case PUT: return "update-"; case PATCH: return "patch-"; default: throw new IllegalArgumentException(method.name()); } } private static Type getType(HttpMethod method) { switch (method) { case GET: return Type.SAFE; case PUT: case DELETE: return Type.IDEMPOTENT; case POST: case PATCH: return Type.UNSAFE; default: return null; } } }