All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fcrepo.http.api.ContentExposingResource Maven / Gradle / Ivy

/*
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree.
 */
package org.fcrepo.http.api;

import static com.google.common.base.Strings.nullToEmpty;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
import static org.apache.jena.graph.NodeFactory.createURI;
import static org.apache.jena.riot.RDFLanguages.contentTypeToLang;
import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate;
import static org.apache.jena.riot.WebContent.ctSPARQLUpdate;
import static org.apache.jena.riot.WebContent.ctTextCSV;
import static org.apache.jena.riot.WebContent.ctTextPlain;
import static org.apache.jena.riot.WebContent.matchContentType;
import static org.fcrepo.http.api.FedoraVersioning.MEMENTO_DATETIME_HEADER;
import static org.fcrepo.http.commons.domain.RDFMediaType.JSON_LD;
import static org.fcrepo.http.commons.domain.RDFMediaType.N3;
import static org.fcrepo.http.commons.domain.RDFMediaType.N3_ALT2;
import static org.fcrepo.http.commons.domain.RDFMediaType.NTRIPLES;
import static org.fcrepo.http.commons.domain.RDFMediaType.RDF_XML;
import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE;
import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_ID_HEADER;
import static org.fcrepo.http.commons.session.TransactionConstants.TX_ENDPOINT_REL;
import static org.fcrepo.http.commons.session.TransactionConstants.TX_PREFIX;
import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL;
import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA;
import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_ID_PREFIX;
import static org.fcrepo.kernel.api.models.ExternalContent.COPY;
import static org.fcrepo.kernel.api.models.ExternalContent.PROXY;
import static org.fcrepo.kernel.api.models.ExternalContent.REDIRECT;
import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_RFC_1123_FORMATTER;
import static org.slf4j.LoggerFactory.getLogger;

import static javax.ws.rs.core.HttpHeaders.ACCEPT;
import static javax.ws.rs.core.HttpHeaders.CACHE_CONTROL;
import static javax.ws.rs.core.HttpHeaders.CONTENT_DISPOSITION;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LOCATION;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.HttpHeaders.LINK;
import static javax.ws.rs.core.MediaType.TEXT_HTML;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
import static javax.ws.rs.core.Response.Status.PARTIAL_CONTENT;
import static javax.ws.rs.core.Response.Status.REQUESTED_RANGE_NOT_SATISFIABLE;
import static javax.ws.rs.core.Response.created;
import static javax.ws.rs.core.Response.noContent;
import static javax.ws.rs.core.Response.notAcceptable;
import static javax.ws.rs.core.Response.ok;
import static javax.ws.rs.core.Response.status;
import static javax.ws.rs.core.Variant.mediaTypes;
import static java.net.URI.create;
import static java.text.MessageFormat.format;
import static java.util.stream.Collectors.toSet;
import static java.util.stream.Stream.empty;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Inject;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.BeanParam;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;

import org.fcrepo.config.DigestAlgorithm;
import org.fcrepo.config.OcflPropsConfig;
import org.fcrepo.http.api.services.EtagService;
import org.fcrepo.http.api.services.HttpRdfService;
import org.fcrepo.http.commons.api.rdf.HttpTripleUtil;
import org.fcrepo.http.commons.domain.MultiPrefer;
import org.fcrepo.http.commons.domain.PreferTag;
import org.fcrepo.http.commons.domain.Range;
import org.fcrepo.http.commons.domain.ldp.LdpPreferTag;
import org.fcrepo.http.commons.responses.RdfNamespacedStream;
import org.fcrepo.kernel.api.RdfStream;
import org.fcrepo.kernel.api.Transaction;
import org.fcrepo.kernel.api.exception.InsufficientStorageException;
import org.fcrepo.kernel.api.exception.InvalidChecksumException;
import org.fcrepo.kernel.api.exception.ItemNotFoundException;
import org.fcrepo.kernel.api.exception.PathNotFoundException;
import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException;
import org.fcrepo.kernel.api.exception.PreconditionException;
import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
import org.fcrepo.kernel.api.exception.ServerManagedTypeException;
import org.fcrepo.kernel.api.exception.TombstoneException;
import org.fcrepo.kernel.api.exception.UnsupportedAlgorithmException;
import org.fcrepo.kernel.api.identifiers.FedoraId;
import org.fcrepo.kernel.api.models.Binary;
import org.fcrepo.kernel.api.models.Container;
import org.fcrepo.kernel.api.models.FedoraResource;
import org.fcrepo.kernel.api.models.NonRdfSourceDescription;
import org.fcrepo.kernel.api.models.TimeMap;
import org.fcrepo.kernel.api.models.Tombstone;
import org.fcrepo.kernel.api.models.WebacAcl;
import org.fcrepo.kernel.api.rdf.DefaultRdfStream;
import org.fcrepo.kernel.api.rdf.RdfNamespaceRegistry;
import org.fcrepo.kernel.api.services.CreateResourceService;
import org.fcrepo.kernel.api.services.DeleteResourceService;
import org.fcrepo.kernel.api.services.ReplacePropertiesService;
import org.fcrepo.kernel.api.services.ResourceTripleService;
import org.fcrepo.kernel.api.services.UpdatePropertiesService;
import org.fcrepo.kernel.api.utils.ContentDigest;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPut;
import org.apache.jena.atlas.web.ContentType;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.Triple;
import org.jvnet.hk2.annotations.Optional;
import org.slf4j.Logger;
import org.springframework.http.ContentDisposition;

/**
 * An abstract class that sits between AbstractResource and any resource that
 * wishes to share the routines for building responses containing binary
 * content.
 *
 * @author Mike Durbin
 * @author ajs6f
 */
public abstract class ContentExposingResource extends FedoraBaseResource {

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

    private static final List VARY_HEADERS = Arrays.asList("Accept", "Range", "Accept-Encoding",
            "Accept-Language");

    static final String INSUFFICIENT_SPACE_IDENTIFYING_MESSAGE = "No space left on device";

    public static final String ACCEPT_DATETIME = "Accept-Datetime";

    static final String ACCEPT_EXTERNAL_CONTENT = "Accept-External-Content-Handling";

    static final String HTTP_HEADER_ACCEPT_PATCH = "Accept-Patch";

    public static final String HTTP_HEADER_OVERWRITE_TOMBSTONE = "Overwrite-Tombstone";

    private static final String HTTP_OCFL_PATH = "Fedora-Ocfl-Path";

    private static final String FCR_PREFIX = "fcr:";
    private static final Set ALLOWED_FCR_PARTS = Set.of(FCR_METADATA, FCR_ACL);

    @Context protected Request request;
    @Context protected HttpServletResponse servletResponse;
    @Context protected ServletContext context;

    @Inject
    @Optional
    private HttpTripleUtil httpTripleUtil;

    @BeanParam
    protected MultiPrefer prefer;

    private FedoraResource fedoraResource;

    @Inject
    protected ExternalContentHandlerFactory extContentHandlerFactory;

    @Inject
    protected RdfNamespaceRegistry namespaceRegistry;

    @Inject
    protected CreateResourceService createResourceService;

    @Inject
    protected DeleteResourceService deleteResourceService;

    @Inject
    protected ReplacePropertiesService replacePropertiesService;

    @Inject
    protected UpdatePropertiesService updatePropertiesService;

    @Inject
    protected EtagService etagService;

    @Inject
    protected HttpRdfService httpRdfService;

    @Inject
    protected ResourceTripleService resourceTripleService;

    @Inject
    protected OcflPropsConfig ocflPropsConfig;

    protected abstract String externalPath();

    protected static final Splitter.MapSplitter RFC3230_SPLITTER =
        Splitter.on(',').omitEmptyStrings().trimResults().withKeyValueSeparator(Splitter.on('=').limit(2));


    /**
     * This method returns an HTTP response with content body appropriate to the following arguments.
     *
     * @param limit is the number of child resources returned in the response, -1 for all
     * @param resource the fedora resource
     * @return HTTP response
     * @throws IOException in case of error extracting content
     */
    protected Response getContent(final int limit, final FedoraResource resource) throws IOException {
        final RdfStream rdfStream = httpRdfService.bodyToExternalStream(getUri(resource).toString(),
                getResourceTriples(limit, resource), identifierConverter());
        final var outputStream = new RdfNamespacedStream(
                    rdfStream, namespaceRegistry.getNamespaces());
        setVaryAndPreferenceAppliedHeaders(servletResponse, prefer, resource);
        return ok(outputStream).build();
    }

    protected void setVaryAndPreferenceAppliedHeaders(final HttpServletResponse servletResponse,
            final MultiPrefer prefer, final FedoraResource resource) {
        if (prefer != null) {
            prefer.getReturn().addResponseHeaders(servletResponse);
        }

        // add vary headers
        final List varyValues = new ArrayList<>(VARY_HEADERS);

        if (resource.isOriginalResource()) {
            varyValues.add(ACCEPT_DATETIME);
        }

        varyValues.forEach(x -> servletResponse.addHeader("Vary", x));
    }

    /**
     * Utility to check if the Prefer header contains handling="lenient"
     * @return True if handling="lenient" was sent.
     */
    protected boolean hasLenientPreferHeader() {
        return (prefer.hasHandling() && prefer.getHandling().getValue().equals("lenient"));
    }

    protected RdfStream getResourceTriples(final FedoraResource resource) {
        return getResourceTriples(-1, resource);
    }

    /**
     * This method returns a stream of RDF triples associated with this target resource
     *
     * @param limit is the number of child resources returned in the response, -1 for all
     * @param resource the fedora resource
     * @return {@link RdfStream}
     */
    private RdfStream getResourceTriples(final int limit, final FedoraResource resource) {
        final LdpPreferTag ldpPreferences = getLdpPreferTag();

        final List> embedStreams = new ArrayList<>();

        try {
            embedStreams.add(resourceTripleService.getResourceTriples(
                    transaction(), resource, ldpPreferences, limit));
        } catch (ItemNotFoundException e) {
            if (resource instanceof Tombstone && ((Tombstone) resource).getDeletedObject().isMemento()) {
                // There is a version created when the object is deleted.
                // This memento has no content file so an ItemNotFound is thrown. Generate a new 410 Gone response.
                final var orig_resource = ((Tombstone) resource).getDeletedObject();
                final var timeMap = identifierConverter().toExternalId(
                        orig_resource.getFedoraId().asTimemap().toString()
                );
                final var tombstone = identifierConverter().toExternalId(
                        resource.getFedoraId().asTombstone().toString()
                );
                throw new TombstoneException(orig_resource, tombstone, timeMap);
            }
            throw e;
        }

        // Embed the children of this object
        if (ldpPreferences.displayEmbed()) {
            final var containedResources = resourceFactory.getChildren(
                    transaction(),
                    resource.getFedoraId());
            embedStreams.add(containedResources.flatMap(child -> resourceTripleService.getResourceTriples(
                    transaction(), child, ldpPreferences, limit)));
        }

        final var rdfStream = new DefaultRdfStream(
                asNode(resource),
                embedStreams.stream().reduce(empty(), Stream::concat)
        );

        if (httpTripleUtil != null && ldpPreferences.displayServerManaged()) {
            // Adds fixity service triple to all resources and transaction triple to repo root.
            return httpTripleUtil.addHttpComponentModelsForResourceToStream(rdfStream, resource, uriInfo);
        }

        return rdfStream;
    }

    private LdpPreferTag getLdpPreferTag() {
        final PreferTag returnPreference;

        if (prefer != null && prefer.hasReturn()) {
            returnPreference = prefer.getReturn();
        } else if (prefer != null && prefer.hasHandling()) {
            returnPreference = prefer.getHandling();
        } else {
            returnPreference = PreferTag.emptyTag();
        }

        return new LdpPreferTag(returnPreference);
    }

    /**
     * Get the binary content of a datastream
     *
     * @param rangeValue the range value
     * @param resource the fedora resource
     * @return Binary blob
     * @throws IOException if io exception occurred
     */
    protected Response getBinaryContent(final String rangeValue, final FedoraResource resource)
            throws IOException {
            final Binary binary = (Binary)resource;
            final CacheControl cc = new CacheControl();
            cc.setMaxAge(0);
            cc.setMustRevalidate(true);
            final Response.ResponseBuilder builder;

            if (rangeValue != null && rangeValue.startsWith("bytes")) {

                final Range range = Range.convert(rangeValue);

                final long contentSize = binary.getContentSize();

                final var rangeOfLength = range.rangeOfLength(contentSize);

                final String contentRangeValue =
                        String.format("bytes %s-%s/%s", rangeOfLength.startAsString(),
                                rangeOfLength.endAsString(), contentSize);

                if (
                    !rangeOfLength.isSatisfiable()
                ) {

                    builder = status(REQUESTED_RANGE_NOT_SATISFIABLE)
                            .header("Content-Range", contentRangeValue);
                } else {
                    final var rangeContent = binary.getRange(rangeOfLength.start(), rangeOfLength.end());

                    builder = status(PARTIAL_CONTENT).entity(rangeContent)
                            .header("Content-Range", contentRangeValue)
                            .header(CONTENT_LENGTH, rangeOfLength.size());
                }

            } else {
                @SuppressWarnings("resource")
                final InputStream content = binary.getContent();
                builder = ok(content);
            }


            // we set the content-type explicitly to avoid content-negotiation from getting in the way
            // getBinaryResourceMediaType will try to use the mime type on the resource, falling back on
            // 'application/octet-stream' if the mime type is syntactically invalid
            return builder.type(getBinaryResourceMediaType(resource).toString())
                    .cacheControl(cc)
                    .build();

        }

    protected URI getUri(final FedoraResource resource) {
        try {
            final String uri = identifierConverter()
                    .toExternalId(resource.getFedoraId().getFullId());
            return new URI(uri);
        } catch (final URISyntaxException e) {
            throw new BadRequestException(e);
        }
    }

    protected FedoraResource resource(final boolean canReturnTombstone) {
        if (fedoraResource == null) {
            try {
                fedoraResource = getResourceFromPath(externalPath(), canReturnTombstone);
            } catch (TombstoneException e) {
                // We now want to display Mementos for a deleted object, so catch the Tombstone exception.
                fedoraResource = e.getFedoraResource();
                if (! fedoraResource.isMemento()) {
                    // Rethrow the exception if we are not requesting a Memento.
                    throw e;
                }
            }
        }

        return fedoraResource;
    }

    protected FedoraResource resource() {
        return resource(false);
    }

    protected FedoraResource reloadResource() {
        this.fedoraResource = null;
        return resource();
    }

    /**
     * Add the standard Accept-Post header, for reuse.
     */
    private void addAcceptPostHeader() {
        final String rdfTypes = TURTLE + "," + N3 + "," + N3_ALT2 + "," + RDF_XML + "," + NTRIPLES + "," + JSON_LD;
        servletResponse.addHeader("Accept-Post", rdfTypes);
    }

    /**
     * Add the standard Accept-External-Content-Handling header, for reuse.
     */
    private void addAcceptExternalHeader() {
        servletResponse.addHeader(ACCEPT_EXTERNAL_CONTENT, COPY + "," + REDIRECT + "," + PROXY);
    }

    private void addMementoHeaders(final FedoraResource resource) {
        if (resource.isMemento()) {
            final Instant mementoInstant = resource.getMementoDatetime();
            if (mementoInstant != null) {
                final String mementoDatetime = MEMENTO_RFC_1123_FORMATTER
                        .format(mementoInstant.atZone(ZoneOffset.UTC));
                servletResponse.addHeader(MEMENTO_DATETIME_HEADER, mementoDatetime);
            }
        }
    }

    private void addArchiveGroupLinkHeader(final FedoraResource resource) {
        resource.getArchivalGroupId().ifPresent(agFedoraId -> {
            try {
                final var uri = new URI(identifierConverter().toExternalId(agFedoraId.getFullId()));
                final var link = buildLink(uri, "archival-group");
                servletResponse.addHeader(LINK, link);
            } catch (final URISyntaxException e) {
                throw new BadRequestException(e);
            }
        });
    }

    protected void addExternalContentHeaders(final FedoraResource resource) {
        if (resource instanceof Binary) {
            final Binary binary = (Binary)resource;

            if (binary.isProxy() || binary.isRedirect()) {
                servletResponse.addHeader(CONTENT_LOCATION, binary.getExternalURL());
            }
        }
    }

    private void addAclHeader(final FedoraResource resource) {
        if (!(resource instanceof WebacAcl) && !resource.isMemento()) {
            final FedoraResource resourceForAcl = resource instanceof TimeMap ?
                    resource.getOriginalResource().getDescribedResource() : resource.getDescribedResource();
            final String resourceUri = getUri(resourceForAcl).toString();
            final String aclLocation =  resourceUri + (resourceUri.endsWith("/") ? "" : "/") + FCR_ACL;
            servletResponse.addHeader(LINK, buildLink(aclLocation, "acl"));
        }
    }

    private void addResourceLinkHeaders(final FedoraResource resource) {
        addResourceLinkHeaders(resource, false);
    }

    private void addResourceLinkHeaders(final FedoraResource resource, final boolean includeAnchor) {
        if (resource instanceof NonRdfSourceDescription) {
            // Link to the original described resource
            final FedoraResource described = resource.getOriginalResource().getDescribedResource();
            final URI uri = getUri(described);
            final Link link = Link.fromUri(uri).rel("describes").build();
            servletResponse.addHeader(LINK, link.toString());
        } else if (resource instanceof Binary) {
            // Link to the original description
            final FedoraResource description = resource.getOriginalResource().getDescription();
            final URI uri = getUri(description);
            final Link.Builder builder = Link.fromUri(uri).rel("describedby");

            if (includeAnchor) {
                builder.param("anchor", getUri(resource).toString());
            }
            servletResponse.addHeader(LINK, builder.build().toString());

            if (resource.isMemento()) {
                final URI mementoDescription = getUri(resource.getDescription());
                servletResponse.addHeader(LINK, buildLink(mementoDescription, "describedby"));
            }
        }

        final boolean isOriginal = resource.isOriginalResource();
        // Add versioning headers for versioned originals and mementos
        if (isOriginal || resource.isMemento() || resource instanceof TimeMap) {
            final URI originalUri = getUri(resource.getOriginalResource());
            try {
                final URI timemapUri = getUri(resource.getTimeMap());
                servletResponse.addHeader(LINK, buildLink(originalUri, "timegate"));
                servletResponse.addHeader(LINK, buildLink(originalUri, "original"));
                servletResponse.addHeader(LINK, buildLink(timemapUri, "timemap"));
            } catch (final PathNotFoundRuntimeException e) {
                LOGGER.debug("TimeMap not found for {}, resource not versioned", getUri(resource));
            }
        }
        // Add all system and user types as Link headers.
        for (final var type : resource.getTypes()) {
            servletResponse.addHeader(LINK, buildLink(type, "type"));
        }
    }

    /**
     * Add Link and Option headers
     *
     * @param resource the resource to generate headers for
     */
    protected void addLinkAndOptionsHttpHeaders(final FedoraResource resource) {
        // Add Link headers
        addResourceLinkHeaders(resource);
        addArchiveGroupLinkHeader(resource);
        addAcceptExternalHeader();

        // Add Options headers
        final String options;
        if (resource.isMemento()) {
            options = "GET,HEAD,OPTIONS";
        } else if (resource instanceof TimeMap) {
            options = (resource.getOriginalResource() instanceof Tombstone ? "HEAD,GET,OPTIONS" :
                    "POST,HEAD,GET,OPTIONS");
            addAcceptPostHeader();
        } else if (resource instanceof Binary) {
            options = "DELETE,HEAD,GET,PUT,OPTIONS";
        } else if (resource instanceof NonRdfSourceDescription) {
            options = "HEAD,GET,DELETE,PUT,PATCH,OPTIONS";
            servletResponse.addHeader(HTTP_HEADER_ACCEPT_PATCH, contentTypeSPARQLUpdate);
        } else if (resource instanceof Container) {
            options = "DELETE,POST,HEAD,GET,PUT,PATCH,OPTIONS";
            servletResponse.addHeader(HTTP_HEADER_ACCEPT_PATCH, contentTypeSPARQLUpdate);
            addAcceptPostHeader();
        } else {
            options = "";
        }

        servletResponse.addHeader("Allow", options);
    }

    protected void addTransactionHeaders(final FedoraResource resource) {
        final var tx = transaction();
        if (!tx.isShortLived()) {
            final var externalId = identifierConverter()
                    .toExternalId(FEDORA_ID_PREFIX + "/" + TX_PREFIX + tx.getId());
            servletResponse.addHeader(ATOMIC_ID_HEADER, externalId);
        }
        if (resource.getFedoraId().isRepositoryRoot()) {
            final var txEndpointUri = identifierConverter()
                    .toExternalId(FEDORA_ID_PREFIX + "/" + TX_PREFIX);
            final Link link = Link.fromUri(txEndpointUri).rel(TX_ENDPOINT_REL).build();
            servletResponse.addHeader(LINK, link.toString());
        }
    }

    /**
     * Utility function for building a Link.
     *
     * @param linkUri String of URI for the link.
     * @param relation the relation string.
     * @return the string version of the link.
     */
    protected static String buildLink(final String linkUri, final String relation) {
        return buildLink(create(linkUri), relation);
    }

    /**
     * Utility function for building a Link.
     *
     * @param linkUri The URI for the link.
     * @param relation the relation string.
     * @return the string version of the link.
     */
    private static String buildLink(final URI linkUri, final String relation) {
        return Link.fromUri(linkUri).rel(relation).build().toString();
    }

    /**
     * Multi-value Link header values parsed by the javax.ws.rs.core are not split out by the framework Therefore we
     * must do this ourselves.
     *
     * @param rawLinks the list of unprocessed links
     * @return List of strings containing one link value per string.
     */
    protected List unpackLinks(final List rawLinks) {
        if (rawLinks == null) {
            return null;
        }

        return rawLinks.stream()
                .flatMap(x -> Arrays.stream(x.split(",")))
                .collect(Collectors.toList());
    }

    protected void addResourceHttpHeaders(final FedoraResource resource) {
        addResourceHttpHeaders(resource, false);
    }

    protected void addResourceHttpHeaders(final FedoraResource resource, final boolean dispositionInline) {
        addResourceHttpHeaders(resource, dispositionInline, false);
    }

    /**
     * Add any resource-specific headers to the response
     * @param resource the resource
     * @param dispositionInline whether to return a binary as Content-Disposition inline
     */
    protected void addResourceHttpHeaders(final FedoraResource resource,
                                          final boolean dispositionInline,
                                          final boolean isRedirect) {
        if (resource instanceof Binary) {
            final Binary binary = (Binary)resource;

            final ContentDisposition.Builder dispositionBuilder = (dispositionInline ? ContentDisposition.inline() :
                    ContentDisposition.attachment());

            // TODO: Size, created-date and modification-date have been deprecated by Spring and the RFC-6266 Appendix B
            dispositionBuilder.size(binary.getContentSize());

            if (binary.getCreatedDate() != null) {
                dispositionBuilder.creationDate(binary.getCreatedDate().atZone(ZoneOffset.UTC));
            }
            if (binary.getLastModifiedDate() != null) {
                dispositionBuilder.modificationDate(binary.getLastModifiedDate().atZone(ZoneOffset.UTC));
            }

            if (StringUtils.isNotBlank(binary.getFilename())) {
                final var encoder = StandardCharsets.ISO_8859_1.newEncoder();

                if (encoder.canEncode(binary.getFilename())) {
                    dispositionBuilder.filename(binary.getFilename());
                } else {
                    dispositionBuilder.filename(binary.getFilename(), StandardCharsets.UTF_8);
                }
            }

            servletResponse.addHeader(CONTENT_TYPE, binary.getMimeType());
            // Returning content-length > 0 causes the client to wait for additional data before following the redirect.
            // final var responseIsRedirect = servletResponse.getStatus() >= 300 && servletResponse.getStatus() < 400;
            if (!binary.isRedirect() && !isRedirect) {
                servletResponse.addHeader(CONTENT_LENGTH, String.valueOf(binary.getContentSize()));
            }
            servletResponse.addHeader("Accept-Ranges", "bytes");
            servletResponse.addHeader(CONTENT_DISPOSITION, dispositionBuilder.build().toString());
        }
        // Add ocflPath header if desired
        if (ocflPropsConfig.isShowPath()) {
            final var contentPath = resource.getStorageRelativePath();
            if (contentPath != null) {
                servletResponse.addHeader(HTTP_OCFL_PATH, contentPath.toString());
            }
        }

        addLinkAndOptionsHttpHeaders(resource);
        addAclHeader(resource);
        addMementoHeaders(resource);
    }

    /**
     * Evaluate the cache control headers for the request to see if it can be served from
     * the cache.
     *
     * @param request the request
     * @param servletResponse the servlet response
     * @param resource the fedora resource
     * @param transaction the transaction
     */
    protected void checkCacheControlHeaders(final Request request,
                                                   final HttpServletResponse servletResponse,
                                                   final FedoraResource resource,
                                                   final Transaction transaction) {
        evaluateRequestPreconditions(request, servletResponse, resource, transaction, true);
        addCacheControlHeaders(servletResponse, resource, transaction);
    }

    /**
     * Add ETag and Last-Modified cache control headers to the response
     * 

* Note: In this implementation, the HTTP headers for ETags and Last-Modified dates are swapped * for fedora:Binary resources and their descriptions. Here, we are drawing a distinction between * the HTTP resource and the LDP resource. As an HTTP resource, the last-modified header should * reflect when the resource at the given URL was last changed. With fedora:Binary resources and * their descriptions, this is a little complicated, for the descriptions have, as their subjects, * the binary itself. And the fedora:lastModified property produced by that NonRdfSourceDescription * refers to the last-modified date of the binary -- not the last-modified date of the * NonRdfSourceDescription. *

* @param servletResponse the servlet response * @param resource the fedora resource * @param transaction the transaction */ protected void addCacheControlHeaders(final HttpServletResponse servletResponse, final FedoraResource resource, final Transaction transaction) { final EntityTag etag; final Instant date; // See note about this code in the javadoc above. if (resource instanceof Binary) { // Use a strong ETag for LDP-NR etag = new EntityTag(resource.getEtagValue()); } else { // Use a weak ETag for the LDP-RS etag = new EntityTag(etagService.getRdfResourceEtag(transaction, resource, getLdpPreferTag(), headers.getAcceptableMediaTypes()), true); } date = resource.getLastModifiedDate(); if (!etag.getValue().isEmpty()) { servletResponse.addHeader("ETag", etag.toString()); } if (resource.getStateToken() != null && !resource.getStateToken().isEmpty()) { //State Tokens, while not used for caching per se, nevertheless belong //here since we can conveniently reuse the value of the etag for //our state token servletResponse.addHeader("X-State-Token", resource.getStateToken()); } if (date != null) { servletResponse.addDateHeader("Last-Modified", date.toEpochMilli()); } } /** * Evaluate request preconditions to ensure the resource is the expected state * @param request the request * @param servletResponse the servlet response * @param resource the resource * @param transaction the transaction */ protected void evaluateRequestPreconditions(final Request request, final HttpServletResponse servletResponse, final FedoraResource resource, final Transaction transaction) { // The resource must be locked prior to applying pre-conditions for the optimistic locking to be effective transaction.lockResource(resource.getFedoraId()); evaluateRequestPreconditions(request, servletResponse, resource, transaction, false); } @VisibleForTesting void evaluateRequestPreconditions(final Request request, final HttpServletResponse servletResponse, final FedoraResource resource, final Transaction transaction, final boolean cacheControl) { if (!transaction.isShortLived()) { // Force cache revalidation if in a transaction servletResponse.addHeader(CACHE_CONTROL, "must-revalidate"); servletResponse.addHeader(CACHE_CONTROL, "max-age=0"); } final EntityTag etag; final Instant date; Instant roundedDate = Instant.now(); // See the related note about the next block of code in the // ContentExposingResource::addCacheControlHeaders method if (resource instanceof Binary) { // Use a strong ETag for the LDP-NR etag = new EntityTag(resource.getEtagValue()); } else { // Use a strong ETag for the LDP-RS when validating If-(None)-Match headers etag = new EntityTag(etagService.getRdfResourceEtag(transaction, resource, getLdpPreferTag(), headers.getAcceptableMediaTypes()), false); } date = resource.getLastModifiedDate(); if (date != null) { roundedDate = date.minusMillis(date.toEpochMilli() % 1000); } Response.ResponseBuilder builder = request.evaluatePreconditions(etag); if ( builder == null ) { builder = request.evaluatePreconditions(Date.from(roundedDate)); } if (builder != null && cacheControl ) { final CacheControl cc = new CacheControl(); cc.setMaxAge(0); cc.setMustRevalidate(true); // here we are implicitly emitting a 304 // the exception is not an error, it's genuinely // an exceptional condition builder = builder.cacheControl(cc).lastModified(Date.from(roundedDate)).tag(etag); } if (builder != null) { final Response response = builder.build(); final Object message = response.getEntity(); throw new PreconditionException(message != null ? message.toString() : "Request failed due to unspecified failed precondition.", response.getStatus()); } final String method = request.getMethod(); if (method.equals(HttpPut.METHOD_NAME) || method.equals(HttpPatch.METHOD_NAME)) { final String stateToken = resource.getStateToken(); final String clientSuppliedStateToken = headers.getHeaderString("X-If-State-Token"); if (clientSuppliedStateToken != null && !stateToken.equals(clientSuppliedStateToken)) { throw new PreconditionException(format( "The client-supplied value ({0}) does not match the current state token ({1}).", clientSuppliedStateToken, stateToken), 412); } } } /** * Returns an acceptable plain text media type if possible, or null if not. * @return an acceptable plain-text media type, or null */ private MediaType acceptablePlainTextMediaType() { final List acceptable = headers.getAcceptableMediaTypes(); if (acceptable == null || acceptable.size() == 0) { return TEXT_PLAIN_TYPE; } for (final MediaType type : acceptable) { if (type.isWildcardType() || (type.isCompatible(TEXT_PLAIN_TYPE) && type.isWildcardSubtype())) { return TEXT_PLAIN_TYPE; } else if (type.isCompatible(TEXT_PLAIN_TYPE)) { return type; } } return null; } /** * Create the appropriate response after a create or update request is processed. When a resource is created, * examine the Prefer and Accept headers to determine whether to include a representation. By default, the URI for * the created resource is return as plain text. If a minimal response is requested, then no body is returned. If a * non-minimal return is requested, return the RDF for the created resource in the appropriate RDF serialization. * * @param resource The created or updated Fedora resource. * @param created True for a newly-created resource, false for an updated resource. * @return 204 No Content (for updated resources), 201 Created (for created resources) including the resource URI or * content depending on Prefer headers. */ @SuppressWarnings("resource") protected Response createUpdateResponse(final FedoraResource resource, final boolean created) { addCacheControlHeaders(servletResponse, resource, transaction()); addResourceLinkHeaders(resource, created); addExternalContentHeaders(resource); addAclHeader(resource); addMementoHeaders(resource); addTransactionHeaders(resource); if (!created) { return noContent().build(); } final URI location = getUri(resource); final Response.ResponseBuilder builder = created(location); if (prefer == null || !prefer.hasReturn()) { final MediaType acceptablePlainText = acceptablePlainTextMediaType(); if (acceptablePlainText != null) { return builder.type(acceptablePlainText).entity(location.toString()).build(); } return notAcceptable(mediaTypes(TEXT_PLAIN_TYPE).build()).build(); } else if (prefer.getReturn().getValue().equals("minimal")) { return builder.build(); } else { prefer.getReturn().addResponseHeaders(servletResponse); final RdfNamespacedStream rdfStream = new RdfNamespacedStream( new DefaultRdfStream(asNode(resource), getResourceTriples(resource)), namespaceRegistry.getNamespaces()); return builder.entity(rdfStream).build(); } } protected static String getSimpleContentType(final MediaType requestContentType) { return requestContentType != null ? requestContentType.getType() + "/" + requestContentType.getSubtype() : null; } protected static boolean isRdfContentType(final String contentTypeString) { final ContentType requestContentType = ContentType.create(contentTypeString); if (requestContentType == null || matchContentType(requestContentType, ctTextPlain) || matchContentType(requestContentType, ctTextCSV)) { // Text files and CSV files are not considered RDF to Fedora, though CSV is a valid // RDF type to Jena (although deprecated). return false; } return (contentTypeToLang(contentTypeString) != null) || matchContentType(requestContentType, ctSPARQLUpdate); } protected void patchResourcewithSparql(final FedoraResource resource, final String requestBody) { updatePropertiesService.updateProperties(transaction(), getUserPrincipal(), resource.getFedoraId(), requestBody); } /** * This method returns a MediaType for a binary resource. * If the resource's media type is syntactically incorrect, it will * return 'application/octet-stream' as the media type. * @param resource the fedora resource * @return the media type of of a binary resource */ protected MediaType getBinaryResourceMediaType(final FedoraResource resource) { try { return MediaType.valueOf(((Binary) resource).getMimeType()); } catch (final IllegalArgumentException e) { LOGGER.warn("Syntactically incorrect MediaType encountered on resource {}: '{}'", resource.getId(), ((Binary)resource).getMimeType()); return MediaType.APPLICATION_OCTET_STREAM_TYPE; } } /** * Create a checksum URI object. * @param checksum the checksum * @return the new URI, or null **/ protected static URI checksumURI( final String checksum ) { if (!isBlank(checksum)) { return create(checksum); } return null; } /** * Calculate the max number of children to display at once. * * @return the limit of children to display. */ protected int getChildrenLimit() { final List acceptHeaders = headers.getRequestHeader(ACCEPT); if (acceptHeaders != null && acceptHeaders.size() > 0) { final List accept = Arrays.asList(acceptHeaders.get(0).split(",")); if (accept.contains(TEXT_HTML)) { // Magic number '100' is tied to common-metadata.vsl display of ellipses return 100; } } final List limits = headers.getRequestHeader("Limit"); if (null != limits && limits.size() > 0) { try { return Integer.parseInt(limits.get(0)); } catch (final NumberFormatException e) { LOGGER.warn("Invalid 'Limit' header value: {}", limits.get(0)); throw new ClientErrorException("Invalid 'Limit' header value: " + limits.get(0), SC_BAD_REQUEST, e); } } return -1; } /** * Check if a path has a segment prefixed with fcr: that is not fcr:metadata or fcr:acl * * @param externalPath the path. */ protected static void hasRestrictedPath(final String externalPath) { final String[] pathSegments = externalPath.split("/"); for (final var part : pathSegments) { if (part.startsWith(FCR_PREFIX) && !ALLOWED_FCR_PARTS.contains(part)) { throw new ServerManagedTypeException("Path cannot contain a fcr: prefixed segment."); } } } /** * Parse the RFC-3230 Digest response header value. Look for a sha1 checksum and return it as a urn, if missing or * malformed an empty string is returned. * * @param digest The Digest header value * @return the sha1 checksum value * @throws UnsupportedAlgorithmException if an unsupported digest is used */ protected static Collection parseDigestHeader(final String digest) throws UnsupportedAlgorithmException { try { final var digestPairs = RFC3230_SPLITTER.split(nullToEmpty(digest)); final var unsupportedAlgs = digestPairs.keySet().stream() .filter(Predicate.not(DigestAlgorithm::isSupportedAlgorithm)) .collect(Collectors.toSet()); // If you have one or more digests that are all valid or no digests. if (digestPairs.isEmpty() || unsupportedAlgs.isEmpty()) { return digestPairs.entrySet().stream() .map(entry -> ContentDigest.asURI(entry.getKey(), entry.getValue())) .collect(toSet()); } else { throw new UnsupportedAlgorithmException(String.format("Unsupported Digest Algorithm%1$s: %2$s", unsupportedAlgs.size() > 1 ? 's' : "", String.join(",", unsupportedAlgs))); } } catch (final IllegalArgumentException e) { throw new ClientErrorException("Invalid Digest header: " + digest + "\n", BAD_REQUEST); } } /** * @param rootThrowable The original throwable * @param throwable The throwable under direct scrutiny. * @throws InvalidChecksumException in case there was a checksum mismatch */ protected void checkForInsufficientStorageException(final Throwable rootThrowable, final Throwable throwable) throws InvalidChecksumException { final String message = throwable.getMessage(); if (throwable instanceof IOException && message != null && message.contains( INSUFFICIENT_SPACE_IDENTIFYING_MESSAGE)) { throw new InsufficientStorageException(throwable.getMessage(), rootThrowable); } if (throwable.getCause() != null) { checkForInsufficientStorageException(rootThrowable, throwable.getCause()); } if (rootThrowable instanceof InvalidChecksumException) { throw (InvalidChecksumException) rootThrowable; } else if (rootThrowable instanceof RuntimeException) { throw (RuntimeException) rootThrowable; } else { throw new RepositoryRuntimeException(rootThrowable.getMessage(), rootThrowable); } } /** * This is a helper method for using the idTranslator to convert this resource into an associated Jena Node. * * @param resource to be converted into a Jena Node * @return the Jena node */ protected Node asNode(final FedoraResource resource) { return createURI(resource.getFedoraId().getFullId()); } /** * Get the FedoraResource for the resource at the external path * @param externalPath the external path * @return the fedora resource at the external path */ private FedoraResource getResourceFromPath(final String externalPath) { return getResourceFromPath(externalPath, false); } /** * Get the FedoraResource for the resource at the external path * @param externalPath the external path * @param canReturnTombstone if tombstones can be returned * @return the fedora resource at the external path */ private FedoraResource getResourceFromPath(final String externalPath, final boolean canReturnTombstone) { final FedoraId fedoraId = identifierConverter().pathToInternalId(externalPath); try { final FedoraResource fedoraResource = resourceFactory.getResource(transaction(), fedoraId); final FedoraResource originalResource; if (fedoraId.isMemento()) { originalResource = fedoraResource.getOriginalResource(); } else { originalResource = fedoraResource; } if (originalResource instanceof Tombstone && !canReturnTombstone) { final String tombstoneUri = identifierConverter().toExternalId( originalResource.getFedoraId().asTombstone().getFullId()); final String timemapUri = identifierConverter().toExternalId( originalResource.getTimeMap().getFedoraId().getFullId()); throw new TombstoneException(fedoraResource, tombstoneUri, timemapUri); } return fedoraResource; } catch (final PathNotFoundException exc) { throw new PathNotFoundRuntimeException(exc.getMessage(), exc); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy