/* * Copyright 2013-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; import static org.springframework.http.HttpMethod.*; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.convert.ConversionService; import org.springframework.data.auditing.AuditableBeanWrapperFactory; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.querydsl.binding.QuerydslPredicate; import org.springframework.data.repository.support.Repositories; import org.springframework.data.repository.support.RepositoryInvoker; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.event.AfterCreateEvent; import org.springframework.data.rest.core.event.AfterDeleteEvent; import org.springframework.data.rest.core.event.AfterSaveEvent; import org.springframework.data.rest.core.event.BeforeCreateEvent; import org.springframework.data.rest.core.event.BeforeDeleteEvent; import org.springframework.data.rest.core.event.BeforeSaveEvent; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.mapping.ResourceType; import org.springframework.data.rest.core.mapping.SearchResourceMappings; import org.springframework.data.rest.core.mapping.SupportedHttpMethods; import org.springframework.data.rest.webmvc.support.BackendId; import org.springframework.data.rest.webmvc.support.DefaultedPageable; import org.springframework.data.rest.webmvc.support.ETag; import org.springframework.data.rest.webmvc.support.ETagDoesntMatchException; import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; import org.springframework.hateoas.PagedResources; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; import org.springframework.hateoas.UriTemplate; 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.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; /** * @author Jon Brisbin * @author Oliver Gierke * @author Greg Turnquist * @author Jeremy Rickard */ @RepositoryRestController class RepositoryEntityController extends AbstractRepositoryRestController implements ApplicationEventPublisherAware { private static final String BASE_MAPPING = "/{repository}"; private static final List<String> ACCEPT_PATCH_HEADERS = Arrays.asList(// RestMediaTypes.MERGE_PATCH_JSON.toString(), // RestMediaTypes.JSON_PATCH_JSON.toString(), // MediaType.APPLICATION_JSON_VALUE); private static final String ACCEPT_HEADER = "Accept"; private static final String LINK_HEADER = "Link"; private final RepositoryEntityLinks entityLinks; private final RepositoryRestConfiguration config; private final HttpHeadersPreparer headersPreparer; private final ResourceStatus resourceStatus; private ApplicationEventPublisher publisher; /** * Creates a new {@link RepositoryEntityController} for the given {@link Repositories}, * {@link RepositoryRestConfiguration}, {@link RepositoryEntityLinks}, {@link PagedResourcesAssembler}, * {@link ConversionService} and {@link AuditableBeanWrapperFactory}. * * @param repositories must not be {@literal null}. * @param config must not be {@literal null}. * @param entityLinks must not be {@literal null}. * @param assembler must not be {@literal null}. * @param auditableBeanWrapperFactory must not be {@literal null}. */ @Autowired public RepositoryEntityController(Repositories repositories, RepositoryRestConfiguration config, RepositoryEntityLinks entityLinks, PagedResourcesAssembler<Object> assembler, HttpHeadersPreparer headersPreparer) { super(assembler); this.entityLinks = entityLinks; this.config = config; this.headersPreparer = headersPreparer; this.resourceStatus = ResourceStatus.of(headersPreparer); } /* * (non-Javadoc) * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher) */ @Override public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } /** * <code>OPTIONS /{repository}</code>. * * @param information * @return * @since 2.2 */ @RequestMapping(value = BASE_MAPPING, method = RequestMethod.OPTIONS) public ResponseEntity<?> optionsForCollectionResource(RootResourceInformation information) { HttpHeaders headers = new HttpHeaders(); SupportedHttpMethods supportedMethods = information.getSupportedMethods(); headers.setAllow(supportedMethods.getMethodsFor(ResourceType.COLLECTION)); return new ResponseEntity<Object>(headers, HttpStatus.OK); } /** * <code>HEAD /{repository}</code> * * @param resourceInformation * @return * @throws HttpRequestMethodNotSupportedException * @since 2.2 */ @RequestMapping(value = BASE_MAPPING, method = RequestMethod.HEAD) public ResponseEntity<?> headCollectionResource(RootResourceInformation resourceInformation, DefaultedPageable pageable) throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.HEAD, ResourceType.COLLECTION); RepositoryInvoker invoker = resourceInformation.getInvoker(); if (null == invoker) { throw new ResourceNotFoundException(); } List<Link> links = getCollectionResourceLinks(resourceInformation, pageable); links.add(0, getDefaultSelfLink()); HttpHeaders headers = new HttpHeaders(); headers.add(LINK_HEADER, new Links(links).toString()); return new ResponseEntity<Object>(headers, HttpStatus.NO_CONTENT); } /** * <code>GET /{repository}</code> - Returns the collection resource (paged or unpaged). * * @param resourceInformation * @param pageable * @param sort * @param assembler * @return * @throws ResourceNotFoundException * @throws HttpRequestMethodNotSupportedException */ @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET) public Resources<?> getCollectionResource(@QuerydslPredicate RootResourceInformation resourceInformation, DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.COLLECTION); RepositoryInvoker invoker = resourceInformation.getInvoker(); if (null == invoker) { throw new ResourceNotFoundException(); } Iterable<?> results = pageable.getPageable() != null ? invoker.invokeFindAll(pageable.getPageable()) : invoker.invokeFindAll(sort); ResourceMetadata metadata = resourceInformation.getResourceMetadata(); Optional<Link> baseLink = Optional.of(entityLinks.linkToPagedResource(resourceInformation.getDomainType(), pageable.isDefault() ? null : pageable.getPageable())); Resources<?> result = toResources(results, assembler, metadata.getDomainType(), baseLink); result.add(getCollectionResourceLinks(resourceInformation, pageable)); return result; } private List<Link> getCollectionResourceLinks(RootResourceInformation resourceInformation, DefaultedPageable pageable) { ResourceMetadata metadata = resourceInformation.getResourceMetadata(); SearchResourceMappings searchMappings = metadata.getSearchResourceMappings(); List<Link> links = new ArrayList<Link>(); links.add(new Link(ProfileController.getPath(this.config, metadata), ProfileResourceProcessor.PROFILE_REL)); if (searchMappings.isExported()) { links.add(entityLinks.linkFor(metadata.getDomainType()).slash(searchMappings.getPath()) .withRel(searchMappings.getRel())); } return links; } @ResponseBody @SuppressWarnings({ "unchecked" }) @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { "application/x-spring-data-compact+json", "text/uri-list" }) public Resources<?> getCollectionResourceCompact(@QuerydslPredicate RootResourceInformation resourceinformation, DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { Resources<?> resources = getCollectionResource(resourceinformation, pageable, sort, assembler); List<Link> links = new ArrayList<Link>(resources.getLinks()); for (Resource<?> resource : ((Resources<Resource<?>>) resources).getContent()) { PersistentEntityResource persistentEntityResource = (PersistentEntityResource) resource; links.add(resourceLink(resourceinformation, persistentEntityResource)); } if (resources instanceof PagedResources) { return new PagedResources<Object>(Collections.emptyList(), ((PagedResources<?>) resources).getMetadata(), links); } else { return new Resources<Object>(Collections.emptyList(), links); } } /** * <code>POST /{repository}</code> - Creates a new entity instances from the collection resource. * * @param resourceInformation * @param payload * @param assembler * @param acceptHeader * @return * @throws HttpRequestMethodNotSupportedException */ @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.POST) public ResponseEntity<ResourceSupport> postCollectionResource(RootResourceInformation resourceInformation, PersistentEntityResource payload, PersistentEntityResourceAssembler assembler, @RequestHeader(value = ACCEPT_HEADER, required = false) String acceptHeader) throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.POST, ResourceType.COLLECTION); return createAndReturn(payload.getContent(), resourceInformation.getInvoker(), assembler, config.returnBodyOnCreate(acceptHeader)); } /** * <code>OPTIONS /{repository}/{id}<code> * * @param information * @return * @since 2.2 */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.OPTIONS) public ResponseEntity<?> optionsForItemResource(RootResourceInformation information) { HttpHeaders headers = new HttpHeaders(); SupportedHttpMethods supportedMethods = information.getSupportedMethods(); headers.setAllow(supportedMethods.getMethodsFor(ResourceType.ITEM)); headers.put("Accept-Patch", ACCEPT_PATCH_HEADERS); return new ResponseEntity<Object>(headers, HttpStatus.OK); } /** * <code>HEAD /{repsoitory}/{id}</code> * * @param resourceInformation * @param id * @return * @throws HttpRequestMethodNotSupportedException * @since 2.2 */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.HEAD) public ResponseEntity<?> headForItemResource(RootResourceInformation resourceInformation, @BackendId Serializable id, PersistentEntityResourceAssembler assembler) throws HttpRequestMethodNotSupportedException { return getItemResource(resourceInformation, id).map(it -> { Links links = new Links(assembler.toResource(it).getLinks()); HttpHeaders headers = headersPreparer.prepareHeaders(resourceInformation.getPersistentEntity(), it); headers.add(LINK_HEADER, links.toString()); return new ResponseEntity<Object>(headers, HttpStatus.NO_CONTENT); }).orElseThrow(() -> new ResourceNotFoundException()); } /** * <code>GET /{repository}/{id}</code> - Returns a single entity. * * @param resourceInformation * @param id * @return * @throws HttpRequestMethodNotSupportedException */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.GET) public ResponseEntity<Resource<?>> getItemResource(RootResourceInformation resourceInformation, @BackendId Serializable id, final PersistentEntityResourceAssembler assembler, @RequestHeader HttpHeaders headers) throws HttpRequestMethodNotSupportedException { return getItemResource(resourceInformation, id).map(it -> { PersistentEntity<?, ?> entity = resourceInformation.getPersistentEntity(); return resourceStatus.getStatusAndHeaders(headers, it, entity).toResponseEntity(// () -> assembler.toFullResource(it)); }).orElseGet(() -> new ResponseEntity<Resource<?>>(HttpStatus.NOT_FOUND)); } /** * <code>PUT /{repository}/{id}</code> - Updates an existing entity or creates one at exactly that place. * * @param resourceInformation * @param payload * @param id * @param assembler * @param eTag * @param acceptHeader * @return * @throws HttpRequestMethodNotSupportedException */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PUT) public ResponseEntity<? extends ResourceSupport> putItemResource(RootResourceInformation resourceInformation, PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler, ETag eTag, @RequestHeader(value = ACCEPT_HEADER, required = false) String acceptHeader) throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.PUT, ResourceType.ITEM); RepositoryInvoker invoker = resourceInformation.getInvoker(); Object objectToSave = payload.getContent(); eTag.verify(resourceInformation.getPersistentEntity(), objectToSave); return payload.isNew() ? createAndReturn(objectToSave, invoker, assembler, config.returnBodyOnCreate(acceptHeader)) : saveAndReturn(objectToSave, invoker, PUT, assembler, config.returnBodyOnUpdate(acceptHeader)); } /** * <code>PATCH /{repository}/{id}</code> - Updates an existing entity or creates one at exactly that place. * * @param resourceInformation * @param payload * @param id * @param assembler * @param eTag, * @param acceptHeader * @return * @throws HttpRequestMethodNotSupportedException * @throws ResourceNotFoundException * @throws ETagDoesntMatchException */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PATCH) public ResponseEntity<ResourceSupport> patchItemResource(RootResourceInformation resourceInformation, PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler, ETag eTag, @RequestHeader(value = ACCEPT_HEADER, required = false) String acceptHeader) throws HttpRequestMethodNotSupportedException, ResourceNotFoundException { resourceInformation.verifySupportedMethod(HttpMethod.PATCH, ResourceType.ITEM); Object domainObject = payload.getContent(); eTag.verify(resourceInformation.getPersistentEntity(), domainObject); return saveAndReturn(domainObject, resourceInformation.getInvoker(), PATCH, assembler, config.returnBodyOnUpdate(acceptHeader)); } /** * <code>DELETE /{repository}/{id}</code> - Deletes the entity backing the item resource. * * @param resourceInformation * @param id * @param eTag * @return * @throws ResourceNotFoundException * @throws HttpRequestMethodNotSupportedException * @throws ETagDoesntMatchException */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.DELETE) public ResponseEntity<?> deleteItemResource(RootResourceInformation resourceInformation, @BackendId Serializable id, ETag eTag) throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.DELETE, ResourceType.ITEM); RepositoryInvoker invoker = resourceInformation.getInvoker(); Optional<Object> domainObj = invoker.invokeFindById(id); return domainObj.map(it -> { PersistentEntity<?, ?> entity = resourceInformation.getPersistentEntity(); eTag.verify(entity, it); publisher.publishEvent(new BeforeDeleteEvent(it)); invoker.invokeDeleteById((Serializable) entity.getIdentifierAccessor(it).getIdentifier().orElse(null)); publisher.publishEvent(new AfterDeleteEvent(it)); return new ResponseEntity<Object>(HttpStatus.NO_CONTENT); }).orElseThrow(() -> new ResourceNotFoundException()); } /** * Merges the given incoming object into the given domain object. * * @param domainObject * @param invoker * @param httpMethod * @return */ private ResponseEntity<ResourceSupport> saveAndReturn(Object domainObject, RepositoryInvoker invoker, HttpMethod httpMethod, PersistentEntityResourceAssembler assembler, boolean returnBody) { publisher.publishEvent(new BeforeSaveEvent(domainObject)); Object obj = invoker.invokeSave(domainObject); publisher.publishEvent(new AfterSaveEvent(obj)); PersistentEntityResource resource = assembler.toFullResource(obj); HttpHeaders headers = headersPreparer.prepareHeaders(Optional.of(resource)); if (PUT.equals(httpMethod)) { addLocationHeader(headers, assembler, obj); } if (returnBody) { return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, resource); } else { return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT, headers); } } /** * Triggers the creation of the domain object and renders it into the response if needed. * * @param domainObject * @param invoker * @return */ private ResponseEntity<ResourceSupport> createAndReturn(Object domainObject, RepositoryInvoker invoker, PersistentEntityResourceAssembler assembler, boolean returnBody) { publisher.publishEvent(new BeforeCreateEvent(domainObject)); Object savedObject = invoker.invokeSave(domainObject); publisher.publishEvent(new AfterCreateEvent(savedObject)); Optional<PersistentEntityResource> resource = Optional .ofNullable(returnBody ? assembler.toFullResource(savedObject) : null); HttpHeaders headers = headersPreparer.prepareHeaders(resource); addLocationHeader(headers, assembler, savedObject); return ControllerUtils.toResponseEntity(HttpStatus.CREATED, headers, resource); } /** * Sets the location header pointing to the resource representing the given instance. Will make sure we properly * expand the URI template potentially created as self link. * * @param headers must not be {@literal null}. * @param assembler must not be {@literal null}. * @param source must not be {@literal null}. */ private void addLocationHeader(HttpHeaders headers, PersistentEntityResourceAssembler assembler, Object source) { String selfLink = assembler.getSelfLinkFor(source).getHref(); headers.setLocation(new UriTemplate(selfLink).expand()); } /** * Returns the object backing the item resource for the given {@link RootResourceInformation} and id. * * @param resourceInformation * @param id * @return * @throws HttpRequestMethodNotSupportedException * @throws {@link ResourceNotFoundException} */ private Optional<Object> getItemResource(RootResourceInformation resourceInformation, Serializable id) throws HttpRequestMethodNotSupportedException, ResourceNotFoundException { resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.ITEM); return resourceInformation.getInvoker().invokeFindById(id); } }