001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.http.api.services;
019
020import static org.slf4j.LoggerFactory.getLogger;
021
022import java.util.Collection;
023import java.util.stream.Collectors;
024
025import javax.inject.Inject;
026import javax.ws.rs.core.MediaType;
027
028import org.fcrepo.kernel.api.ContainmentIndex;
029import org.fcrepo.kernel.api.Transaction;
030import org.fcrepo.kernel.api.models.FedoraResource;
031import org.fcrepo.kernel.api.rdf.LdpTriplePreferences;
032import org.fcrepo.kernel.api.services.MembershipService;
033
034import org.apache.commons.codec.digest.DigestUtils;
035import org.slf4j.Logger;
036import org.springframework.stereotype.Component;
037
038/**
039 * Service for computing etags for request responses
040 *
041 * @author bbpennel
042 */
043@Component
044public class EtagService {
045    private static final Logger LOGGER = getLogger(EtagService.class);
046
047    @Inject
048    private ContainmentIndex containmentIndex;
049
050    @Inject
051    private MembershipService membershipService;
052
053    /**
054     * Produces etag for a request for an RDF resource. It is based on factors related to the
055     * current state of the resource, as well as request options which change the
056     * representation of the resource.
057     *
058     * @param transaction transaction
059     * @param resource resource
060     * @param prefers LDP preference headers for the request
061     * @param acceptableMediaTypes collection of acceptable media types for the response
062     * @return etag for the request
063     */
064    public String getRdfResourceEtag(final Transaction transaction, final FedoraResource resource,
065                                     final LdpTriplePreferences prefers,
066                                     final Collection<MediaType> acceptableMediaTypes) {
067        // Start etag based on the current state of the fedora resource, using the state token
068        final StringBuilder etag = new StringBuilder(resource.getStateToken());
069
070        // Factor in the requested mimetype(s)
071        final String mimetype = acceptableMediaTypes.stream()
072                .map(MediaType::toString)
073                .sorted()
074                .collect(Collectors.joining(";"));
075        addComponent(etag, mimetype);
076
077        // Factor in preferences which change which triples are included in the response
078        etag.append('|');
079        if (prefers.displayContainment()) {
080            final var lastUpdated = containmentIndex.containmentLastUpdated(transaction, resource.getFedoraId());
081            if (lastUpdated != null) {
082                etag.append(lastUpdated);
083            }
084        }
085        etag.append('|');
086        if (prefers.displayMembership()) {
087            final var lastUpdated = membershipService.getLastUpdatedTimestamp(transaction, resource.getFedoraId());
088            if (lastUpdated != null) {
089                etag.append(lastUpdated);
090            }
091        }
092        addComponent(etag, prefers.displayEmbed());
093        addComponent(etag, prefers.displayUserRdf());
094        addComponent(etag, prefers.displayReferences());
095        addComponent(etag, prefers.displayServerManaged());
096
097        // Compute a digest of all these components to use as the etag
098        final String etagMd5 = DigestUtils.md5Hex(etag.toString()).toUpperCase();
099        LOGGER.debug("Produced etag {} for {} from {}", etagMd5, resource.getId(), etag);
100
101        return etagMd5;
102    }
103
104    private void addComponent(final StringBuilder etag, final Object component) {
105        etag.append('|').append(component);
106    }
107}