/*
* Copyright 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.mapping;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.SimpleAssociationHandler;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.core.mapping.ResourceMapping;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.support.SelfLinkProvider;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.util.Assert;
/**
* A service to collect all standard links that need to be added to a certain object.
*
* @author Oliver Gierke
*/
public class LinkCollector {
private final PersistentEntities entities;
private final Associations associationLinks;
private final SelfLinkProvider links;
/**
* Creates a new {@link PersistentEntities}, {@link SelfLinkProvider} and {@link Associations}.
*
* @param entities must not be {@literal null}.
* @param linkProvider must not be {@literal null}.
* @param associationLinks must not be {@literal null}.
*/
public LinkCollector(PersistentEntities entities, SelfLinkProvider linkProvider, Associations associationLinks) {
Assert.notNull(entities, "PersistentEntities must not be null!");
Assert.notNull(linkProvider, "SelfLinkProvider must not be null!");
Assert.notNull(associationLinks, "AssociationLinks must not be null!");
this.links = linkProvider;
this.entities = entities;
this.associationLinks = associationLinks;
}
/**
* Returns all {@link Links} for the given object.
*
* @param object must not be {@literal null}.
* @return
*/
public Links getLinksFor(Object object) {
return getLinksFor(object, Collections.<Link> emptyList());
}
/**
* Returns all {@link Links} for the given object and already existing {@link Link}.
*
* @param object must not be {@literal null}.
* @param existingLinks must not be {@literal null}.
* @return
*/
public Links getLinksFor(Object object, List<Link> existingLinks) {
Assert.notNull(object, "Object must not be null!");
Assert.notNull(existingLinks, "Existing links must not be null!");
Links links = new Links(existingLinks);
Link selfLink = createSelfLink(object, links);
if (selfLink == null) {
return links;
}
Path path = new Path(selfLink.expand().getHref());
LinkCollectingAssociationHandler handler = new LinkCollectingAssociationHandler(entities, path, associationLinks);
entities.getRequiredPersistentEntity(object.getClass()).doWithAssociations(handler);
List<Link> result = new ArrayList<Link>(existingLinks);
result.addAll(handler.getLinks());
return addSelfLinkIfNecessary(object, result);
}
public Links getLinksForNested(Object object, List<Link> existing) {
PersistentEntity<?, ?> entity = entities.getRequiredPersistentEntity(object.getClass());
NestedLinkCollectingAssociationHandler handler = new NestedLinkCollectingAssociationHandler(links,
entity.getPropertyAccessor(object), associationLinks);
entity.doWithAssociations(handler);
List<Link> links = new ArrayList<Link>();
links.addAll(existing);
links.addAll(handler.getLinks());
return new Links(links);
}
private Links addSelfLinkIfNecessary(Object object, List<Link> existing) {
Links result = new Links(existing);
if (result.hasLink(Link.REL_SELF)) {
return result;
}
List<Link> list = new ArrayList<Link>();
list.add(createSelfLink(object, result));
list.addAll(existing);
return new Links(list);
}
private Link createSelfLink(Object object, Links existing) {
if (existing.hasLink(Link.REL_SELF)) {
return existing.getLink(Link.REL_SELF);
}
return links.createSelfLinkFor(object).withSelfRel();
}
/**
* {@link SimpleAssociationHandler} that will collect {@link Link}s for all linkable associations.
*
* @author Oliver Gierke
* @since 2.1
*/
@RequiredArgsConstructor
private static class LinkCollectingAssociationHandler implements SimpleAssociationHandler {
private static final String AMBIGUOUS_ASSOCIATIONS = "Detected multiple association links with same relation type! Disambiguate association %s using @RestResource!";
private final @NonNull PersistentEntities entities;
private final @NonNull Path basePath;
private final @NonNull Associations associationLinks;
private final @NonNull List<Link> links = new ArrayList<Link>();
/**
* Returns the links collected after the {@link Association} has been traversed.
*
* @return the links
*/
public List<Link> getLinks() {
return links;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association)
*/
@Override
public void doWithAssociation(final Association<? extends PersistentProperty<?>> association) {
if (associationLinks.isLinkableAssociation(association)) {
PersistentProperty<?> property = association.getInverse();
Links existingLinks = new Links(links);
for (Link link : associationLinks.getLinksFor(association, basePath)) {
if (existingLinks.hasLink(link.getRel())) {
throw new MappingException(String.format(AMBIGUOUS_ASSOCIATIONS, property.toString()));
} else {
links.add(link);
}
}
}
}
}
@RequiredArgsConstructor
private static class NestedLinkCollectingAssociationHandler implements SimpleAssociationHandler {
private final SelfLinkProvider selfLinks;
private final PersistentPropertyAccessor accessor;
private final Associations associations;
private final @Getter List<Link> links = new ArrayList<Link>();
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association)
*/
@Override
public void doWithAssociation(Association<? extends PersistentProperty<?>> association) {
if (!associations.isLinkableAssociation(association)) {
return;
}
PersistentProperty<?> property = association.getInverse();
accessor.getProperty(property).ifPresent(it -> {
ResourceMetadata metadata = associations.getMappings().getMetadataFor(property.getOwner().getType());
ResourceMapping propertyMapping = metadata.getMappingFor(property);
for (Object element : asCollection(it)) {
if (element != null)
links.add(getLinkFor(element, propertyMapping));
}
});
}
/**
* Returns a link pointing to the given entity using the given {@link ResourceMapping} to detect the link relation.
*
* @param entity must not be {@literal null}.
* @param mapping must not be {@literal null}.
* @return
*/
private Link getLinkFor(Object entity, ResourceMapping mapping) {
return selfLinks.createSelfLinkFor(entity).withRel(mapping.getRel());
}
/**
* Returns the given object as {@link Collection}, i.e. the object as is if it's a collection already or wrapped
* into a single-element collection otherwise.
*
* @param object can be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
private static Collection<Object> asCollection(Object object) {
if (object instanceof Collection) {
return (Collection<Object>) object;
}
return Collections.singleton(object);
}
}
}