Java tutorial
/* * 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.trellisldp.http; import static java.time.Instant.MAX; import static java.util.Arrays.asList; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import static java.util.Optional.ofNullable; import static javax.ws.rs.Priorities.AUTHORIZATION; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.CONFLICT; import static javax.ws.rs.core.Response.Status.GONE; import static javax.ws.rs.core.Response.Status.METHOD_NOT_ALLOWED; import static javax.ws.rs.core.Response.Status.NOT_FOUND; import static javax.ws.rs.core.Response.seeOther; import static javax.ws.rs.core.Response.status; import static javax.ws.rs.core.UriBuilder.fromUri; import static org.slf4j.LoggerFactory.getLogger; import static org.trellisldp.api.RDFUtils.TRELLIS_PREFIX; import static org.trellisldp.http.domain.HttpConstants.TIMEMAP; import static org.trellisldp.http.impl.RdfUtils.ldpResourceTypes; import com.codahale.metrics.annotation.Timed; import java.io.File; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.Priority; import javax.inject.Singleton; import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HEAD; import javax.ws.rs.OPTIONS; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.PreMatching; import javax.ws.rs.core.Link; import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; import org.apache.commons.rdf.api.IRI; import org.slf4j.Logger; import org.trellisldp.api.BinaryService; import org.trellisldp.api.IOService; import org.trellisldp.api.Resource; import org.trellisldp.api.ResourceService; import org.trellisldp.http.domain.AcceptDatetime; import org.trellisldp.http.domain.Digest; import org.trellisldp.http.domain.LdpRequest; import org.trellisldp.http.domain.PATCH; import org.trellisldp.http.domain.Prefer; import org.trellisldp.http.domain.Range; import org.trellisldp.http.domain.Version; import org.trellisldp.http.impl.DeleteHandler; import org.trellisldp.http.impl.GetHandler; import org.trellisldp.http.impl.MementoResource; import org.trellisldp.http.impl.OptionsHandler; import org.trellisldp.http.impl.PatchHandler; import org.trellisldp.http.impl.PostHandler; import org.trellisldp.http.impl.PutHandler; import org.trellisldp.http.impl.RdfUtils; import org.trellisldp.vocabulary.LDP; /** * @author acoburn */ @PreMatching @Provider @Priority(AUTHORIZATION + 20) @Singleton @Path("{partition}{path: .*}") public class PartitionedLdpResource extends BaseLdpResource implements ContainerRequestFilter { private static final Logger LOGGER = getLogger(PartitionedLdpResource.class); protected final ResourceService resourceService; protected final IOService ioService; protected final BinaryService binaryService; private static final List<String> MUTATING_METHODS = asList("POST", "PUT", "DELETE", "PATCH"); /** * Create a partitioned LdpResource * @param resourceService the resource service * @param ioService the i/o service * @param binaryService the datastream service * @param partitions a map of partitions for use with custom hostnames */ public PartitionedLdpResource(final ResourceService resourceService, final IOService ioService, final BinaryService binaryService, final Map<String, String> partitions) { super(partitions); this.resourceService = resourceService; this.ioService = ioService; this.binaryService = binaryService; } @Override public void filter(final ContainerRequestContext ctx) throws IOException { // Check for a trailing slash. If so, redirect final String path = ctx.getUriInfo().getPath(); if (path.endsWith("/")) { ctx.abortWith(seeOther(fromUri(path.substring(0, path.length() - 1)).build()).build()); } // Validate header/query parameters ofNullable(ctx.getHeaderString("Accept-Datetime")).ifPresent(x -> { if (isNull(AcceptDatetime.valueOf(x))) { ctx.abortWith(status(BAD_REQUEST).build()); } }); ofNullable(ctx.getHeaderString("Slug")).filter(s -> s.contains("/")) .ifPresent(x -> ctx.abortWith(status(BAD_REQUEST).build())); ofNullable(ctx.getHeaderString("Prefer")).ifPresent(x -> { if (isNull(Prefer.valueOf(x))) { ctx.abortWith(status(BAD_REQUEST).build()); } }); ofNullable(ctx.getHeaderString("Range")).ifPresent(x -> { if (isNull(Range.valueOf(x))) { ctx.abortWith(status(BAD_REQUEST).build()); } }); ofNullable(ctx.getHeaderString("Link")).ifPresent(x -> { try { Link.valueOf(x); } catch (final IllegalArgumentException ex) { ctx.abortWith(status(BAD_REQUEST).build()); } }); ofNullable(ctx.getHeaderString("Digest")).ifPresent(x -> { if (isNull(Digest.valueOf(x))) { ctx.abortWith(status(BAD_REQUEST).build()); } }); ofNullable(ctx.getUriInfo().getQueryParameters().getFirst("version")).ifPresent(x -> { // Check well-formedness if (isNull(Version.valueOf(x))) { ctx.abortWith(status(BAD_REQUEST).build()); // Do not allow mutating versioned resources } else if (MUTATING_METHODS.contains(ctx.getMethod())) { ctx.abortWith(status(METHOD_NOT_ALLOWED).build()); } }); // Do not allow direct manipulation of timemaps ofNullable(ctx.getUriInfo().getQueryParameters().get("ext")).filter(l -> l.contains(TIMEMAP)) .filter(x -> MUTATING_METHODS.contains(ctx.getMethod())) .ifPresent(x -> ctx.abortWith(status(METHOD_NOT_ALLOWED).build())); } /** * Perform a GET operation on an LDP Resource * @param req the request parameters * @return the response * * Note: The Memento implemenation pattern exactly follows * <a href="https://tools.ietf.org/html/rfc7089#section-4.2.1">section 4.2.1 of RFC 7089</a>. */ @GET @Timed public Response getResource(@BeanParam final LdpRequest req) { return fetchResource(req); } /** * Perform a HEAD operation on an LDP Resource * @param req the request parameters * @return the response * * Note: The Memento implemenation pattern exactly follows * <a href="https://tools.ietf.org/html/rfc7089#section-4.2.1">section 4.2.1 of RFC 7089</a>. */ @HEAD @Timed public Response getResourceHeaders(@BeanParam final LdpRequest req) { return fetchResource(req); } private Response fetchResource(final LdpRequest req) { final String baseUrl = partitions.get(req.getPartition()); final IRI identifier = rdf.createIRI(TRELLIS_PREFIX + req.getPartition() + req.getPath()); final GetHandler getHandler = new GetHandler(req, resourceService, ioService, binaryService, baseUrl); // Fetch a versioned resource if (nonNull(req.getVersion())) { LOGGER.info("Getting versioned resource: {}", req.getVersion()); return resourceService.get(identifier, req.getVersion().getInstant()).map(getHandler::getRepresentation) .orElseGet(() -> status(NOT_FOUND)).build(); // Fetch a timemap } else if (TIMEMAP.equals(req.getExt())) { LOGGER.info("Getting timemap resource"); return resourceService.get(identifier).map(MementoResource::new) .map(res -> res.getTimeMapBuilder(req, ioService, baseUrl)).orElseGet(() -> status(NOT_FOUND)) .build(); // Fetch a timegate } else if (nonNull(req.getDatetime())) { LOGGER.info("Getting timegate resource: {}", req.getDatetime().getInstant()); return resourceService.get(identifier, req.getDatetime().getInstant()).map(MementoResource::new) .map(res -> res.getTimeGateBuilder(req, baseUrl)).orElseGet(() -> status(NOT_FOUND)).build(); } // Fetch the current state of the resource LOGGER.info("Getting resource at: {}", identifier); return resourceService.get(identifier).map(getHandler::getRepresentation).orElseGet(() -> status(NOT_FOUND)) .build(); } /** * Perform an OPTIONS operation on an LDP Resource * @param req the request * @return the response */ @OPTIONS @Timed public Response options(@BeanParam final LdpRequest req) { final String baseUrl = partitions.get(req.getPartition()); final IRI identifier = rdf.createIRI(TRELLIS_PREFIX + req.getPartition() + req.getPath()); final OptionsHandler optionsHandler = new OptionsHandler(req, resourceService, baseUrl); if (nonNull(req.getVersion())) { return resourceService.get(identifier, req.getVersion().getInstant()).map(optionsHandler::ldpOptions) .orElseGet(() -> status(NOT_FOUND)).build(); } return resourceService.get(identifier).map(optionsHandler::ldpOptions).orElseGet(() -> status(NOT_FOUND)) .build(); } /** * Perform a PATCH operation on an LDP Resource * @param req the request * @param body the body * @return the response */ @PATCH @Timed @Consumes("application/sparql-update") public Response updateResource(@BeanParam final LdpRequest req, final String body) { final String baseUrl = partitions.get(req.getPartition()); final IRI identifier = rdf.createIRI(TRELLIS_PREFIX + req.getPartition() + req.getPath()); final PatchHandler patchHandler = new PatchHandler(req, body, resourceService, ioService, baseUrl); return resourceService.get(identifier, MAX).map(patchHandler::updateResource) .orElseGet(() -> status(NOT_FOUND)).build(); } /** * Perform a DELETE operation on an LDP Resource * @param req the request * @return the response */ @DELETE @Timed public Response deleteResource(@BeanParam final LdpRequest req) { final String baseUrl = partitions.get(req.getPartition()); final IRI identifier = rdf.createIRI(TRELLIS_PREFIX + req.getPartition() + req.getPath()); final DeleteHandler deleteHandler = new DeleteHandler(req, resourceService, baseUrl); return resourceService.get(identifier, MAX).map(deleteHandler::deleteResource) .orElseGet(() -> status(NOT_FOUND)).build(); } /** * Perform a POST operation on a LDP Resource * @param req the request * @param body the body * @return the response */ @POST @Timed public Response createResource(@BeanParam final LdpRequest req, final File body) { final String baseUrl = partitions.get(req.getPartition()); final String path = req.getPartition() + req.getPath(); final String identifier = "/" + ofNullable(req.getSlug()).orElseGet(resourceService.getIdentifierSupplier()); final PostHandler postHandler = new PostHandler(req, identifier, body, resourceService, ioService, binaryService, baseUrl); // First check if this is a container final Optional<Resource> parent = resourceService.get(rdf.createIRI(TRELLIS_PREFIX + path)); if (parent.isPresent()) { final Optional<IRI> ixModel = parent.map(Resource::getInteractionModel); if (ixModel.filter(type -> ldpResourceTypes(type).anyMatch(LDP.Container::equals)).isPresent()) { return resourceService.get(rdf.createIRI(TRELLIS_PREFIX + path + identifier), MAX) .map(x -> status(CONFLICT)).orElseGet(postHandler::createResource).build(); } else if (parent.filter(RdfUtils::isDeleted).isPresent()) { return status(GONE).build(); } return status(METHOD_NOT_ALLOWED).build(); } return status(NOT_FOUND).build(); } /** * Perform a PUT operation on a LDP Resource * @param req the request * @param body the body * @return the response */ @PUT @Timed public Response setResource(@BeanParam final LdpRequest req, final File body) { if (!partitions.containsKey(req.getPartition())) { LOGGER.warn("Partition {} not defined in configuration", req.getPartition()); return status(NOT_FOUND).build(); } final String baseUrl = partitions.get(req.getPartition()); final IRI identifier = rdf.createIRI(TRELLIS_PREFIX + req.getPartition() + req.getPath()); final PutHandler putHandler = new PutHandler(req, body, resourceService, ioService, binaryService, baseUrl); return resourceService.get(identifier, MAX).filter(res -> !RdfUtils.isDeleted(res)) .map(putHandler::setResource).orElseGet(putHandler::createResource).build(); } }