/* * Copyright 2012-2015 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; import static org.springframework.data.rest.webmvc.ControllerUtils.*; import static org.springframework.data.rest.webmvc.RestMediaTypes.*; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; import static org.springframework.web.bind.annotation.RequestMethod.*; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.function.Function; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.CollectionFactory; import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.repository.support.Repositories; import org.springframework.data.repository.support.RepositoryInvoker; import org.springframework.data.repository.support.RepositoryInvokerFactory; import org.springframework.data.rest.core.event.AfterLinkDeleteEvent; import org.springframework.data.rest.core.event.AfterLinkSaveEvent; import org.springframework.data.rest.core.event.BeforeLinkDeleteEvent; import org.springframework.data.rest.core.event.BeforeLinkSaveEvent; import org.springframework.data.rest.core.mapping.PropertyAwareResourceMapping; import org.springframework.data.rest.core.mapping.ResourceMapping; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.webmvc.support.BackendId; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; import org.springframework.hateoas.mvc.ControllerLinkBuilder; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; /** * @author Jon Brisbin * @author Oliver Gierke * @author Greg Turnquist */ @RepositoryRestController @SuppressWarnings({ "unchecked" }) class RepositoryPropertyReferenceController extends AbstractRepositoryRestController implements ApplicationEventPublisherAware { private static final String BASE_MAPPING = "/{repository}/{id}/{property}"; private static final Collection<HttpMethod> AUGMENTING_METHODS = Arrays.asList(HttpMethod.PATCH, HttpMethod.POST); private final Repositories repositories; private final RepositoryInvokerFactory repositoryInvokerFactory; private ApplicationEventPublisher publisher; @Autowired public RepositoryPropertyReferenceController(Repositories repositories, RepositoryInvokerFactory repositoryInvokerFactory, PagedResourcesAssembler<Object> assembler) { super(assembler); this.repositories = repositories; this.repositoryInvokerFactory = repositoryInvokerFactory; } /* * (non-Javadoc) * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher) */ @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } @RequestMapping(value = BASE_MAPPING, method = GET) public ResponseEntity<ResourceSupport> followPropertyReference(final RootResourceInformation repoRequest, @BackendId Serializable id, final @PathVariable String property, final PersistentEntityResourceAssembler assembler) throws Exception { final HttpHeaders headers = new HttpHeaders(); Function<ReferencedProperty, ResourceSupport> handler = prop -> prop.propertyValue.map(it -> { if (prop.property.isCollectionLike()) { return toResources((Iterable<?>) it, assembler, prop.propertyType, Optional.empty()); } else if (prop.property.isMap()) { Map<Object, Resource<?>> resources = new HashMap<Object, Resource<?>>(); for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) it).entrySet()) { resources.put(entry.getKey(), assembler.toResource(entry.getValue())); } return new Resource<Object>(resources); } else { PersistentEntityResource resource = assembler.toResource(it); headers.set("Content-Location", resource.getId().getHref()); return resource; } }).orElseThrow(() -> new ResourceNotFoundException()); return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, // doWithReferencedProperty(repoRequest, id, property, handler, HttpMethod.GET)); } @RequestMapping(value = BASE_MAPPING, method = DELETE) public ResponseEntity<? extends ResourceSupport> deletePropertyReference(final RootResourceInformation repoRequest, @BackendId Serializable id, @PathVariable String property) throws Exception { Function<ReferencedProperty, ResourceSupport> handler = prop -> prop.propertyValue.map(it -> { if (prop.property.isCollectionLike() || prop.property.isMap()) { throw HttpRequestMethodNotSupportedException.forRejectedMethod(HttpMethod.DELETE) .withAllowedMethods(HttpMethod.GET, HttpMethod.HEAD); } else { prop.wipeValue(); } publisher.publishEvent(new BeforeLinkDeleteEvent(prop.accessor.getBean(), prop.propertyValue)); Object result = repoRequest.getInvoker().invokeSave(prop.accessor.getBean()); publisher.publishEvent(new AfterLinkDeleteEvent(result, prop.propertyValue)); return (ResourceSupport) null; }).orElse(null); doWithReferencedProperty(repoRequest, id, property, handler, HttpMethod.DELETE); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } @RequestMapping(value = BASE_MAPPING + "/{propertyId}", method = GET) public ResponseEntity<ResourceSupport> followPropertyReference(final RootResourceInformation repoRequest, @BackendId Serializable id, @PathVariable String property, final @PathVariable String propertyId, final PersistentEntityResourceAssembler assembler) throws Exception { final HttpHeaders headers = new HttpHeaders(); Function<ReferencedProperty, ResourceSupport> handler = prop -> prop.propertyValue.map(it -> { if (prop.property.isCollectionLike()) { for (Object obj : (Iterable<?>) it) { IdentifierAccessor accessor1 = prop.entity.getIdentifierAccessor(obj); if (propertyId.equals(accessor1.getIdentifier().toString())) { PersistentEntityResource resource1 = assembler.toResource(obj); headers.set("Content-Location", resource1.getId().getHref()); return resource1; } } } else if (prop.property.isMap()) { for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) it).entrySet()) { IdentifierAccessor accessor2 = prop.entity.getIdentifierAccessor(entry.getValue()); if (propertyId.equals(accessor2.getIdentifier().toString())) { PersistentEntityResource resource2 = assembler.toResource(entry.getValue()); headers.set("Content-Location", resource2.getId().getHref()); return resource2; } } } else { return new Resource<Object>(prop.propertyValue); } throw new ResourceNotFoundException(); }).orElseThrow(() -> new ResourceNotFoundException()); return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, // doWithReferencedProperty(repoRequest, id, property, handler, HttpMethod.GET)); } @RequestMapping(value = BASE_MAPPING, method = GET, produces = { SPRING_DATA_COMPACT_JSON_VALUE, TEXT_URI_LIST_VALUE }) public ResponseEntity<ResourceSupport> followPropertyReferenceCompact(RootResourceInformation repoRequest, @BackendId Serializable id, @PathVariable String property, PersistentEntityResourceAssembler assembler) throws Exception { ResponseEntity<ResourceSupport> response = followPropertyReference(repoRequest, id, property, assembler); if (response.getStatusCode() != HttpStatus.OK) { return response; } ResourceMetadata repoMapping = repoRequest.getResourceMetadata(); PersistentProperty<?> persistentProp = repoRequest.getPersistentEntity().getRequiredPersistentProperty(property); ResourceMapping propertyMapping = repoMapping.getMappingFor(persistentProp); ResourceSupport resource = response.getBody(); List<Link> links = new ArrayList<Link>(); ControllerLinkBuilder linkBuilder = linkTo(methodOn(RepositoryPropertyReferenceController.class) .followPropertyReference(repoRequest, id, property, assembler)); if (resource instanceof Resource) { Object content = ((Resource<?>) resource).getContent(); if (content instanceof Iterable) { for (Resource<?> res : (Iterable<Resource<?>>) content) { links.add(linkBuilder.withRel(propertyMapping.getRel())); } } else if (content instanceof Map) { Map<Object, Resource<?>> map = (Map<Object, Resource<?>>) content; for (Entry<Object, Resource<?>> entry : map.entrySet()) { Link l = new Link(entry.getValue().getLink("self").getHref(), entry.getKey().toString()); links.add(l); } } } else { links.add(linkBuilder.withRel(propertyMapping.getRel())); } return ControllerUtils.toResponseEntity(HttpStatus.OK, null, new Resource<Object>(EMPTY_RESOURCE_LIST, links)); } @RequestMapping(value = BASE_MAPPING, method = { PATCH, PUT, POST }, // consumes = { MediaType.APPLICATION_JSON_VALUE, SPRING_DATA_COMPACT_JSON_VALUE, TEXT_URI_LIST_VALUE }) public ResponseEntity<? extends ResourceSupport> createPropertyReference( final RootResourceInformation resourceInformation, final HttpMethod requestMethod, final @RequestBody(required = false) Resources<Object> incoming, @BackendId Serializable id, @PathVariable String property) throws Exception { final Resources<Object> source = incoming == null ? new Resources<Object>(Collections.emptyList()) : incoming; final RepositoryInvoker invoker = resourceInformation.getInvoker(); Function<ReferencedProperty, ResourceSupport> handler = prop -> { Class<?> propertyType = prop.property.getType(); if (prop.property.isCollectionLike()) { Collection<Object> collection = AUGMENTING_METHODS.contains(requestMethod) ? (Collection<Object>) prop.propertyValue.orElse(null) : CollectionFactory.createCollection(propertyType, 0); // Add to the existing collection for (Link l1 : source.getLinks()) { collection.add(loadPropertyValue(prop.propertyType, l1).orElse(null)); } prop.accessor.setProperty(prop.property, Optional.of(collection)); } else if (prop.property.isMap()) { Map<String, Object> map = AUGMENTING_METHODS.contains(requestMethod) ? (Map<String, Object>) prop.propertyValue.orElse(null) : CollectionFactory.<String, Object> createMap(propertyType, 0); // Add to the existing collection for (Link l2 : source.getLinks()) { map.put(l2.getRel(), loadPropertyValue(prop.propertyType, l2).orElse(null)); } prop.accessor.setProperty(prop.property, Optional.of(map)); } else { if (HttpMethod.PATCH.equals(requestMethod)) { throw HttpRequestMethodNotSupportedException.forRejectedMethod(HttpMethod.PATCH)// .withAllowedMethods(HttpMethod.PATCH)// .withMessage( "Cannot PATCH a reference to this singular property since the property type is not a List or a Map."); } if (source.getLinks().size() != 1) { throw new IllegalArgumentException( "Must send only 1 link to update a property reference that isn't a List or a Map."); } Optional<Object> propVal = loadPropertyValue(prop.propertyType, source.getLinks().get(0)); prop.accessor.setProperty(prop.property, propVal); } publisher.publishEvent(new BeforeLinkSaveEvent(prop.accessor.getBean(), prop.propertyValue)); Object result = invoker.invokeSave(prop.accessor.getBean()); publisher.publishEvent(new AfterLinkSaveEvent(result, prop.propertyValue)); return null; }; doWithReferencedProperty(resourceInformation, id, property, handler, requestMethod); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } @RequestMapping(value = BASE_MAPPING + "/{propertyId}", method = DELETE) public ResponseEntity<ResourceSupport> deletePropertyReferenceId(final RootResourceInformation repoRequest, @BackendId Serializable backendId, @PathVariable String property, final @PathVariable String propertyId) throws Exception { Function<ReferencedProperty, ResourceSupport> handler = prop -> prop.propertyValue.map(it -> { if (prop.property.isCollectionLike()) { Collection<Object> coll = (Collection<Object>) it; Iterator<Object> iterator = coll.iterator(); while (iterator.hasNext()) { Object obj = iterator.next(); prop.entity.getIdentifierAccessor(obj).getIdentifier()// .map(Object::toString)// .filter(id -> propertyId.equals(id))// .ifPresent(__ -> iterator.remove()); } } else if (prop.property.isMap()) { Map<Object, Object> m = (Map<Object, Object>) it; Iterator<Entry<Object, Object>> iterator = m.entrySet().iterator(); while (iterator.hasNext()) { Object key = iterator.next().getKey(); prop.entity.getIdentifierAccessor(m.get(key)).getIdentifier()// .map(Object::toString)// .filter(id -> propertyId.equals(id))// .ifPresent(__ -> iterator.remove()); } } else { prop.wipeValue(); } publisher.publishEvent(new BeforeLinkDeleteEvent(prop.accessor.getBean(), it)); Object result = repoRequest.getInvoker().invokeSave(prop.accessor.getBean()); publisher.publishEvent(new AfterLinkDeleteEvent(result, it)); return (ResourceSupport) null; }).orElse(null); doWithReferencedProperty(repoRequest, backendId, property, handler, HttpMethod.DELETE); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } private Optional<Object> loadPropertyValue(Class<?> type, Link link) { String href = link.expand().getHref(); String id = href.substring(href.lastIndexOf('/') + 1); RepositoryInvoker invoker = repositoryInvokerFactory.getInvokerFor(type); return invoker.invokeFindById(id); } private Optional<ResourceSupport> doWithReferencedProperty(RootResourceInformation resourceInformation, Serializable id, String propertyPath, Function<ReferencedProperty, ResourceSupport> handler, HttpMethod method) throws Exception { ResourceMetadata metadata = resourceInformation.getResourceMetadata(); PropertyAwareResourceMapping mapping = metadata.getProperty(propertyPath); if (mapping == null || !mapping.isExported()) { throw new ResourceNotFoundException(); } PersistentProperty<?> property = mapping.getProperty(); resourceInformation.verifySupportedMethod(method, property); RepositoryInvoker invoker = resourceInformation.getInvoker(); Optional<Object> domainObj = invoker.invokeFindById(id); domainObj.orElseThrow(() -> new ResourceNotFoundException()); return domainObj.map(it -> { PersistentPropertyAccessor accessor = property.getOwner().getPropertyAccessor(it); return handler.apply(new ReferencedProperty(property, accessor.getProperty(property), accessor)); }); } private class ReferencedProperty { final PersistentEntity<?, ?> entity; final PersistentProperty<?> property; final Class<?> propertyType; final Optional<Object> propertyValue; final PersistentPropertyAccessor accessor; private ReferencedProperty(PersistentProperty<?> property, Optional<Object> propertyValue, PersistentPropertyAccessor wrapper) { this.property = property; this.propertyValue = propertyValue; this.accessor = wrapper; this.propertyType = property.getActualType(); this.entity = repositories.getPersistentEntity(propertyType); } public void writeValue() { accessor.setProperty(property, propertyValue); } public void wipeValue() { accessor.setProperty(property, Optional.empty()); } } @ExceptionHandler public ResponseEntity<Void> handle(HttpRequestMethodNotSupportedException exception) { return exception.toResponse(); } @RequiredArgsConstructor(access = AccessLevel.PRIVATE) static class HttpRequestMethodNotSupportedException extends RuntimeException { private static final long serialVersionUID = 3704212056962845475L; private final HttpMethod rejectedMethod; private final HttpMethod[] allowedMethods; private final String message; public static HttpRequestMethodNotSupportedException forRejectedMethod(HttpMethod method) { return new HttpRequestMethodNotSupportedException(method, new HttpMethod[0], null); } public HttpRequestMethodNotSupportedException withAllowedMethods(HttpMethod... methods) { return new HttpRequestMethodNotSupportedException(this.rejectedMethod, methods.clone(), null); } public HttpRequestMethodNotSupportedException withMessage(String message, Object... parameters) { return new HttpRequestMethodNotSupportedException(this.rejectedMethod, this.allowedMethods, String.format(message, parameters)); } /* * (non-Javadoc) * @see java.lang.Throwable#getMessage() */ @Override public String getMessage() { return message; } public ResponseEntity<Void> toResponse() { return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).allow(allowedMethods).build(); } } }