package nl.knaw.huygens.alexandria.endpoint.resource; /* * #%L * alexandria-main * ======= * Copyright (C) 2015 - 2017 Huygens ING (KNAW) * ======= * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ import static nl.knaw.huygens.alexandria.api.EndpointPaths.ANNOTATIONS; import static nl.knaw.huygens.alexandria.api.EndpointPaths.ANNOTATORS; import static nl.knaw.huygens.alexandria.api.EndpointPaths.RESOURCES; import static nl.knaw.huygens.alexandria.api.EndpointPaths.SUBRESOURCES; import static nl.knaw.huygens.alexandria.api.EndpointPaths.TEXT; import static nl.knaw.huygens.alexandria.endpoint.resource.ResourceValidatorFactory.resourceNotFoundForId; import java.util.Optional; import javax.inject.Inject; import javax.inject.Singleton; import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import nl.knaw.huygens.alexandria.api.model.AlexandriaState; import nl.knaw.huygens.alexandria.api.model.StatePrototype; import nl.knaw.huygens.alexandria.endpoint.JSONEndpoint; import nl.knaw.huygens.alexandria.endpoint.LocationBuilder; import nl.knaw.huygens.alexandria.endpoint.UUIDParam; import nl.knaw.huygens.alexandria.exception.BadRequestException; import nl.knaw.huygens.alexandria.exception.ConflictException; import nl.knaw.huygens.alexandria.model.AlexandriaResource; import nl.knaw.huygens.alexandria.service.AlexandriaService; @Singleton @Path(RESOURCES) @Api(RESOURCES) public class ResourcesEndpoint extends JSONEndpoint { private final AlexandriaService service; private final ResourceEntityBuilder entityBuilder; private final ResourceCreationRequestBuilder requestBuilder; private final LocationBuilder locationBuilder; @Inject public ResourcesEndpoint(AlexandriaService service, // ResourceCreationRequestBuilder requestBuilder, // LocationBuilder locationBuilder, // ResourceEntityBuilder entityBuilder) { this.service = service; this.locationBuilder = locationBuilder; this.entityBuilder = entityBuilder; this.requestBuilder = requestBuilder; // Log.trace("Resources created, service=[{}]", service); } @GET @Path("{uuid}") @ApiOperation(value = "Get the resource with the given uuid", response = ResourceEntity.class) public Response getResourceByID(@PathParam("uuid") final UUIDParam uuidParam) { return ok(readExistingResource(uuidParam)); } @POST @Consumes(MediaType.APPLICATION_JSON) @ApiOperation("create new Resource") public Response createResource(@NotNull @Valid @WithoutId ResourcePrototype protoType) { // Log.trace("protoType=[{}]", protoType); protoType.setState(AlexandriaState.TENTATIVE); final ResourceCreationRequest request = requestBuilder.build(protoType); AlexandriaResource resource = request.execute(service); if (request.wasExecutedAsIs()) { return noContent(); } return created(resource); } @PUT @Path("{uuid}/state") @Consumes(MediaType.APPLICATION_JSON) @ApiOperation(value = "update the state of the resource (only state=CONFIRMED accepted for now)") public Response setResourceState(@PathParam("uuid") final UUIDParam uuidParam, @NotNull StatePrototype protoType) { // Log.trace("protoType=[{}]", protoType); AlexandriaResource resource = readExistingResource(uuidParam); if (protoType.isConfirmed()) { if (!resource.isActive()) { throw new ConflictException(resource.getState() + " resources cannot be set to CONFIRMED"); } service.confirmResource(resource.getId()); return noContent(); } throw new BadRequestException("for now, you can only set the state to CONFIRMED"); } @DELETE @Path("{uuid}") public Response deleteNotSupported(@PathParam("uuid") final UUIDParam paramId) { return methodNotImplemented(); } @PUT @Path("{uuid}") @Consumes(MediaType.APPLICATION_JSON) @ApiOperation(value = "update/create the resource with the given uuid") public Response setResourceAtSpecificID(@PathParam("uuid") final UUIDParam uuid, // @NotNull @Valid @MatchesPathId ResourcePrototype protoType) { // Log.trace("protoType=[{}]", protoType); protoType.setState(AlexandriaState.CONFIRMED); protoType.setId(uuid); // in case the prototype has no id, get it from the Path Optional<AlexandriaResource> existingResource = service.readResource(uuid.getValue()); if (existingResource.isPresent() && isNotConfirmed(existingResource.get())) { throw new ConflictException("This resource has state " + existingResource.get().getState() + "; only CONFIRMED resources can be updated."); } final ResourceCreationRequest request = requestBuilder.build(protoType); AlexandriaResource resource = request.execute(service); if (request.newResourceWasCreated()) { return created(resource); } if (request.wasExecutedAsIs()) { return noContent(); } return ok(resource); } // Sub-resource delegation @Path("{uuid}/" + SUBRESOURCES) public Class<SubResourcesEndpoint> getSubResourcesEndpoint(@PathParam("uuid") final UUIDParam uuidParam) { assertResourceIsConfirmed(uuidParam); return SubResourcesEndpoint.class; // no instantiation of our own; let Jersey handle the lifecycle } @Path("{uuid}/" + ANNOTATIONS) public Class<ResourceAnnotationsEndpoint> getAnnotationsEndpoint(@PathParam("uuid") final UUIDParam uuidParam) { assertResourceIsConfirmed(uuidParam); return ResourceAnnotationsEndpoint.class; // no instantiation of our own; let Jersey handle the lifecycle } @Path("{uuid}/" + ANNOTATORS) public Class<ResourceAnnotatorsEndpoint> getAnnotatorsEndpoint(@PathParam("uuid") final UUIDParam uuidParam) { assertResourceIsConfirmed(uuidParam); return ResourceAnnotatorsEndpoint.class; // no instantiation of our own; let Jersey handle the lifecycle } @Path("{uuid}/" + TEXT) public Class<ResourceTextEndpoint> getResourceTextEndpoint(@PathParam("uuid") final UUIDParam uuidParam) { assertResourceIsConfirmed(uuidParam); return ResourceTextEndpoint.class; // no instantiation of our own; let Jersey handle the lifecycle } @Path("{uuid}/provenance") public ResourceProvenanceEndpoint getProvenanceEndpoint(@PathParam("uuid") final UUIDParam uuidParam) { // If we let Jersey handle the lifecycle, this endpoint doesn't show up in the standard application.wadl // TODO: make the combination subresource as class/application.wadl work swagger seems to have the same problem return new ResourceProvenanceEndpoint(service, uuidParam, locationBuilder); } private Response created(AlexandriaResource resource) { return created(locationBuilder.locationOf(resource)); } private Response ok(AlexandriaResource resource) { return ok(entityBuilder.build(resource)); } private AlexandriaResource readExistingResource(UUIDParam id) { return service.readResource(id.getValue()).orElseThrow(resourceNotFoundForId(id)); } private boolean isNotConfirmed(AlexandriaResource resource) { return !resource.getState().equals(AlexandriaState.CONFIRMED); } private void assertResourceIsConfirmed(UUIDParam uuidParam) { AlexandriaResource resource = readExistingResource(uuidParam); if (isNotConfirmed(resource)) { throw new ConflictException("This resource has state " + resource.getState() + "; it needs to be CONFIRMED first."); } } }