org.trellisldp.http.impl.GetHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.trellisldp.http.impl.GetHandler.java

Source

/*
 * 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.impl;

import static java.lang.String.join;
import static java.util.Collections.singletonList;
import static java.util.Date.from;
import static java.util.Objects.isNull;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Stream.of;
import static javax.ws.rs.HttpMethod.DELETE;
import static javax.ws.rs.HttpMethod.GET;
import static javax.ws.rs.HttpMethod.HEAD;
import static javax.ws.rs.HttpMethod.OPTIONS;
import static javax.ws.rs.HttpMethod.POST;
import static javax.ws.rs.HttpMethod.PUT;
import static javax.ws.rs.core.HttpHeaders.ALLOW;
import static javax.ws.rs.core.HttpHeaders.VARY;
import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
import static javax.ws.rs.core.MediaType.TEXT_HTML;
import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.ok;
import static org.apache.commons.codec.digest.DigestUtils.md5Hex;
import static org.apache.commons.rdf.api.RDFSyntax.TURTLE;
import static org.slf4j.LoggerFactory.getLogger;
import static org.trellisldp.http.domain.HttpConstants.ACCEPT_DATETIME;
import static org.trellisldp.http.domain.HttpConstants.ACCEPT_PATCH;
import static org.trellisldp.http.domain.HttpConstants.ACCEPT_POST;
import static org.trellisldp.http.domain.HttpConstants.ACCEPT_RANGES;
import static org.trellisldp.http.domain.HttpConstants.ACL;
import static org.trellisldp.http.domain.HttpConstants.DIGEST;
import static org.trellisldp.http.domain.HttpConstants.LINK_TEMPLATE;
import static org.trellisldp.http.domain.HttpConstants.MEMENTO_DATETIME;
import static org.trellisldp.http.domain.HttpConstants.PATCH;
import static org.trellisldp.http.domain.HttpConstants.PREFER;
import static org.trellisldp.http.domain.HttpConstants.PREFERENCE_APPLIED;
import static org.trellisldp.http.domain.HttpConstants.RANGE;
import static org.trellisldp.http.domain.HttpConstants.WANT_DIGEST;
import static org.trellisldp.http.domain.Prefer.PREFER_MINIMAL;
import static org.trellisldp.http.domain.Prefer.PREFER_REPRESENTATION;
import static org.trellisldp.http.domain.Prefer.PREFER_RETURN;
import static org.trellisldp.http.domain.RdfMediaType.APPLICATION_SPARQL_UPDATE;
import static org.trellisldp.http.domain.RdfMediaType.MEDIA_TYPES;
import static org.trellisldp.http.impl.RdfUtils.filterWithLDF;
import static org.trellisldp.http.impl.RdfUtils.filterWithPrefer;
import static org.trellisldp.http.impl.RdfUtils.getDefaultProfile;
import static org.trellisldp.http.impl.RdfUtils.getProfile;
import static org.trellisldp.http.impl.RdfUtils.getSyntax;
import static org.trellisldp.http.impl.RdfUtils.ldpResourceTypes;
import static org.trellisldp.http.impl.RdfUtils.unskolemizeQuads;
import static org.trellisldp.vocabulary.OA.annotationService;
import static org.trellisldp.vocabulary.Trellis.PreferAccessControl;
import static org.trellisldp.vocabulary.Trellis.PreferUserManaged;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.Instant;
import java.util.Optional;
import java.util.stream.Stream;

import javax.ws.rs.NotFoundException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.StreamingOutput;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BoundedInputStream;
import org.apache.commons.rdf.api.IRI;
import org.apache.commons.rdf.api.Quad;
import org.apache.commons.rdf.api.RDFSyntax;
import org.slf4j.Logger;

import org.trellisldp.api.Binary;
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.LdpRequest;
import org.trellisldp.http.domain.Prefer;
import org.trellisldp.http.domain.WantDigest;
import org.trellisldp.vocabulary.LDP;
import org.trellisldp.vocabulary.Memento;

/**
 * The GET response builder
 *
 * @author acoburn
 */
public class GetHandler extends BaseLdpHandler {

    private static final Logger LOGGER = getLogger(GetHandler.class);

    private final IOService ioService;
    private final BinaryService binaryService;

    /**
     * A GET response builder
     * @param req the LDP request
     * @param resourceService the resource service
     * @param ioService the serialization service
     * @param binaryService the binary service
     * @param baseUrl the base URL
     */
    public GetHandler(final LdpRequest req, final ResourceService resourceService, final IOService ioService,
            final BinaryService binaryService, final String baseUrl) {
        super(req, resourceService, baseUrl);
        this.ioService = ioService;
        this.binaryService = binaryService;
    }

    /**
     * Build the representation for the given resource
     * @param res the resource
     * @return the response builder
     */
    public ResponseBuilder getRepresentation(final Resource res) {
        final String identifier = getBaseUrl() + req.getPartition() + req.getPath();

        // Check if this is already deleted
        checkDeleted(res, identifier);

        LOGGER.debug("Acceptable media types: {}", req.getHeaders().getAcceptableMediaTypes());
        final Optional<RDFSyntax> syntax = getSyntax(req.getHeaders().getAcceptableMediaTypes(),
                res.getBinary().map(b -> b.getMimeType().orElse(APPLICATION_OCTET_STREAM)));

        if (ACL.equals(req.getExt()) && !res.hasAcl()) {
            throw new NotFoundException();
        }

        final ResponseBuilder builder = basicGetResponseBuilder(res, syntax);

        // Add NonRDFSource-related "describe*" link headers
        res.getBinary().ifPresent(ds -> {
            if (syntax.isPresent()) {
                builder.link(identifier + "#description", "canonical").link(identifier, "describes");
            } else {
                builder.link(identifier, "canonical").link(identifier + "#description", "describedby")
                        .type(ds.getMimeType().orElse(APPLICATION_OCTET_STREAM));
            }
        });

        // Only show memento links for the user-managed graph (not ACL)
        if (!ACL.equals(req.getExt())) {
            builder.link(identifier, "original timegate")
                    .links(MementoResource.getMementoLinks(identifier, res.getMementos()).toArray(Link[]::new));
        }

        // URI Template
        builder.header(LINK_TEMPLATE,
                "<" + identifier + "{?version}>; rel=\"" + Memento.Memento.getIRIString() + "\"");

        // NonRDFSources responses (strong ETags, etc)
        if (res.getBinary().isPresent() && !syntax.isPresent()) {
            return getLdpNr(identifier, res, builder);
        }

        // RDFSource responses (weak ETags, etc)
        final RDFSyntax s = syntax.orElse(TURTLE);
        final IRI profile = getProfile(req.getHeaders().getAcceptableMediaTypes(), s);
        return getLdpRs(identifier, res, builder, s, profile);
    }

    private ResponseBuilder getLdpRs(final String identifier, final Resource res, final ResponseBuilder builder,
            final RDFSyntax syntax, final IRI profile) {

        // Check for a cache hit
        final EntityTag etag = new EntityTag(md5Hex(res.getModified() + identifier), true);
        checkCache(req.getRequest(), res.getModified(), etag);

        builder.tag(etag);
        if (res.isMemento()) {
            builder.header(ALLOW, join(",", GET, HEAD, OPTIONS));
        } else if (ACL.equals(req.getExt())) {
            builder.header(ALLOW, join(",", GET, HEAD, OPTIONS, PATCH));
        } else if (res.getInteractionModel().equals(LDP.RDFSource)) {
            builder.header(ALLOW, join(",", GET, HEAD, OPTIONS, PATCH, PUT, DELETE));
        } else {
            builder.header(ALLOW, join(",", GET, HEAD, OPTIONS, PATCH, PUT, DELETE, POST));
        }

        // URI Templates
        builder.header(LINK_TEMPLATE,
                "<" + identifier + "{?subject,predicate,object}>; rel=\"" + LDP.Resource.getIRIString() + "\"");

        final Prefer prefer = ACL
                .equals(req.getExt())
                        ? new Prefer(PREFER_REPRESENTATION, singletonList(PreferAccessControl.getIRIString()),
                                of(PreferUserManaged, LDP.PreferContainment, LDP.PreferMembership)
                                        .map(IRI::getIRIString).collect(toList()),
                                null, null, null)
                        : req.getPrefer();

        ofNullable(prefer).ifPresent(p -> builder.header(PREFERENCE_APPLIED,
                PREFER_RETURN + "=" + p.getPreference().orElse(PREFER_REPRESENTATION)));

        if (ofNullable(prefer).flatMap(Prefer::getPreference).filter(PREFER_MINIMAL::equals).isPresent()) {
            return builder.status(NO_CONTENT);
        }

        // Short circuit HEAD requests
        if (HEAD.equals(req.getRequest().getMethod())) {
            return builder;
        }

        // Stream the rdf content
        final StreamingOutput stream = new StreamingOutput() {
            @Override
            public void write(final OutputStream out) throws IOException {
                try (final Stream<? extends Quad> stream = res.stream()) {
                    ioService.write(
                            stream.filter(filterWithPrefer(prefer))
                                    .map(unskolemizeQuads(resourceService, getBaseUrl()))
                                    .filter(filterWithLDF(req.getSubject(), req.getPredicate(), req.getObject()))
                                    .map(Quad::asTriple),
                            out, syntax,
                            ofNullable(profile).orElseGet(() -> getDefaultProfile(syntax, identifier)));
                }
            }
        };
        return builder.entity(stream);
    }

    private ResponseBuilder getLdpNr(final String identifier, final Resource res, final ResponseBuilder builder) {

        final Instant mod = res.getBinary().map(Binary::getModified).orElseThrow(
                () -> new WebApplicationException("Could not access binary metadata for " + res.getIdentifier()));
        final EntityTag etag = new EntityTag(md5Hex(mod + identifier + "BINARY"));
        checkCache(req.getRequest(), mod, etag);

        // Set last-modified to be the binary's last-modified value
        builder.lastModified(from(mod));

        final IRI dsid = res.getBinary().map(Binary::getIdentifier).orElseThrow(
                () -> new WebApplicationException("Could not access binary metadata for " + res.getIdentifier()));

        builder.header(VARY, RANGE).header(VARY, WANT_DIGEST).header(ACCEPT_RANGES, "bytes").tag(etag);

        if (res.isMemento()) {
            builder.header(ALLOW, join(",", GET, HEAD, OPTIONS));
        } else {
            builder.header(ALLOW, join(",", GET, HEAD, OPTIONS, PUT, DELETE));
        }

        // Add instance digests, if Requested and supported
        ofNullable(req.getWantDigest()).map(WantDigest::getAlgorithms).ifPresent(
                algs -> algs.stream().filter(binaryService.supportedAlgorithms()::contains).findFirst().ifPresent(
                        alg -> getBinaryDigest(dsid, alg).ifPresent(digest -> builder.header(DIGEST, digest))));

        // Stream the binary content
        final StreamingOutput stream = new StreamingOutput() {
            @Override
            public void write(final OutputStream out) throws IOException {
                // TODO -- with JDK 9 use InputStream::transferTo instead of IOUtils::copy
                try (final InputStream binary = binaryService.getContent(req.getPartition(), dsid)
                        .orElseThrow(() -> new IOException("Could not retrieve content from " + dsid))) {
                    if (isNull(req.getRange())) {
                        IOUtils.copy(binary, out);
                    } else {
                        // Range Requests
                        final long skipped = binary.skip(req.getRange().getFrom());
                        if (skipped < req.getRange().getFrom()) {
                            LOGGER.warn("Trying to skip more data available in the input stream! {}, {}", skipped,
                                    req.getRange().getFrom());
                        }
                        try (final InputStream sliced = new BoundedInputStream(binary,
                                req.getRange().getTo() - req.getRange().getFrom())) {
                            IOUtils.copy(sliced, out);
                        }
                    }
                } catch (final IOException ex) {
                    throw new WebApplicationException("Error processing binary content: " + ex.getMessage());
                }
            }
        };

        return builder.entity(stream);
    }

    private Optional<String> getBinaryDigest(final IRI dsid, final String algorithm) {
        final Optional<InputStream> b = binaryService.getContent(req.getPartition(), dsid);
        try (final InputStream is = b
                .orElseThrow(() -> new WebApplicationException("Couldn't fetch binary content"))) {
            return binaryService.digest(algorithm, is);
        } catch (final IOException ex) {
            LOGGER.error("Error computing digest on content: {}", ex.getMessage());
            throw new WebApplicationException("Error handling binary content: " + ex.getMessage());
        }
    }

    private ResponseBuilder basicGetResponseBuilder(final Resource res, final Optional<RDFSyntax> syntax) {
        final ResponseBuilder builder = ok();

        // Standard HTTP Headers
        builder.lastModified(from(res.getModified()));

        final IRI model;

        if (isNull(req.getExt())) {
            syntax.ifPresent(s -> {
                builder.header(VARY, PREFER);
                builder.type(s.mediaType);
            });

            model = res.getBinary().isPresent() && syntax.isPresent() ? LDP.RDFSource : res.getInteractionModel();
            // Link headers from User data
            res.getTypes().forEach(type -> builder.link(type.getIRIString(), "type"));
            res.getInbox().map(IRI::getIRIString).ifPresent(inbox -> builder.link(inbox, "inbox"));
            res.getAnnotationService().map(IRI::getIRIString)
                    .ifPresent(svc -> builder.link(svc, annotationService.getIRIString()));
        } else {
            model = LDP.RDFSource;
        }

        // Add LDP-required headers
        ldpResourceTypes(model).forEach(type -> {
            builder.link(type.getIRIString(), "type");
            // Mementos don't accept POST or PATCH
            if (LDP.Container.equals(type) && !res.isMemento()) {
                builder.header(ACCEPT_POST, MEDIA_TYPES.stream().map(mt -> mt.getType() + "/" + mt.getSubtype())
                        // text/html is excluded
                        .filter(mt -> !TEXT_HTML.equals(mt)).collect(joining(",")));
            } else if (LDP.RDFSource.equals(type) && !res.isMemento()) {
                builder.header(ACCEPT_PATCH, APPLICATION_SPARQL_UPDATE);
            }
        });

        // Memento-related headers
        if (res.isMemento()) {
            builder.header(MEMENTO_DATETIME, from(res.getModified()));
        } else {
            builder.header(VARY, ACCEPT_DATETIME);
        }

        return builder;
    }
}