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;
019
020import com.google.common.annotations.VisibleForTesting;
021import io.micrometer.core.annotation.Timed;
022
023import org.fcrepo.http.commons.responses.HtmlTemplate;
024import org.fcrepo.http.commons.responses.LinkFormatStream;
025import org.fcrepo.kernel.api.exception.CannotCreateMementoException;
026import org.fcrepo.kernel.api.exception.InvalidChecksumException;
027import org.fcrepo.kernel.api.exception.MementoDatetimeFormatException;
028import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
029import org.fcrepo.kernel.api.models.FedoraResource;
030import org.slf4j.Logger;
031import org.springframework.context.annotation.Scope;
032
033import javax.servlet.http.HttpServletResponse;
034import javax.ws.rs.BadRequestException;
035import javax.ws.rs.DELETE;
036import javax.ws.rs.GET;
037import javax.ws.rs.HeaderParam;
038import javax.ws.rs.OPTIONS;
039import javax.ws.rs.POST;
040import javax.ws.rs.Path;
041import javax.ws.rs.PathParam;
042import javax.ws.rs.Produces;
043import javax.ws.rs.core.Context;
044import javax.ws.rs.core.Link;
045import javax.ws.rs.core.Link.Builder;
046import javax.ws.rs.core.Request;
047import javax.ws.rs.core.Response;
048import javax.ws.rs.core.UriInfo;
049import java.io.IOException;
050import java.net.URI;
051import java.time.Instant;
052import java.time.ZoneId;
053import java.util.ArrayList;
054import java.util.Arrays;
055import java.util.Comparator;
056import java.util.List;
057import java.util.stream.Collectors;
058
059import static javax.ws.rs.core.Response.Status.METHOD_NOT_ALLOWED;
060import static javax.ws.rs.core.Response.ok;
061import static javax.ws.rs.core.Response.status;
062
063import static org.fcrepo.http.commons.domain.RDFMediaType.APPLICATION_LINK_FORMAT;
064import static org.fcrepo.http.commons.domain.RDFMediaType.JSON_LD;
065import static org.fcrepo.http.commons.domain.RDFMediaType.N3_ALT2_WITH_CHARSET;
066import static org.fcrepo.http.commons.domain.RDFMediaType.N3_WITH_CHARSET;
067import static org.fcrepo.http.commons.domain.RDFMediaType.NTRIPLES;
068import static org.fcrepo.http.commons.domain.RDFMediaType.RDF_XML;
069import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_HTML_WITH_CHARSET;
070import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_PLAIN_WITH_CHARSET;
071import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_WITH_CHARSET;
072import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_X;
073import static org.fcrepo.kernel.api.FedoraTypes.FCR_VERSIONS;
074import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_RFC_1123_FORMATTER;
075import static org.slf4j.LoggerFactory.getLogger;
076
077/**
078 * @author cabeer
079 * @since 9/25/14
080 */
081@Timed
082@Scope("request")
083@Path("/{path: .*}/fcr:versions")
084public class FedoraVersioning extends ContentExposingResource {
085
086    private static final Logger LOGGER = getLogger(FedoraVersioning.class);
087
088    @VisibleForTesting
089    public static final String MEMENTO_DATETIME_HEADER = "Memento-Datetime";
090
091    @Context protected Request request;
092    @Context protected HttpServletResponse servletResponse;
093    @Context protected UriInfo uriInfo;
094
095    @PathParam("path") protected String externalPath;
096
097
098    /**
099     * Default JAX-RS entry point
100     */
101    public FedoraVersioning() {
102        super();
103    }
104
105    /**
106     * Create a new FedoraNodes instance for a given path
107     * @param externalPath the external path
108     */
109    @VisibleForTesting
110    public FedoraVersioning(final String externalPath) {
111        this.externalPath = externalPath;
112    }
113
114    /**
115     * Create a new version of a resource. If a memento-datetime header is provided, then the new version will be
116     * based off the provided body using that datetime. If one was not provided, then a version is created based off
117     * the current version of the resource.
118     *
119     * @return response
120     * @throws InvalidChecksumException thrown if one of the provided digests does not match the content
121     * @throws MementoDatetimeFormatException if the header value of memento-datetime is not RFC-1123 format
122     */
123    @POST
124    public Response addVersion() {
125
126        if (headers.getHeaderString("Slug") != null) {
127            throw new BadRequestException("Slug header is no longer supported for versioning label.");
128        }
129
130        if (headers.getHeaderString(MEMENTO_DATETIME_HEADER) != null) {
131            throw new CannotCreateMementoException(MEMENTO_DATETIME_HEADER +
132                    " header is no longer supported on versioning.");
133        }
134
135        final var transaction = transaction();
136
137        if (!transaction.isShortLived()) {
138            throw new BadRequestException("Version creation is not allowed within transactions.");
139        }
140
141        final var resource = resource();
142
143        try {
144            LOGGER.debug("Request to create version for <{}>", externalPath);
145
146            doInDbTxWithRetry(() -> {
147                versionService.createVersion(transaction, resource.getFedoraId(), getUserPrincipal());
148
149                // need to commit the transaction before loading the memento otherwise it won't exist
150                transaction.commitIfShortLived();
151            });
152
153            final var versions = reloadResource().getTimeMap().getChildren().collect(Collectors.toList());
154
155            if (versions.isEmpty()) {
156                throw new RepositoryRuntimeException(String.format("Failed to create a version for %s", externalPath));
157            }
158
159            final var memento = versions.get(versions.size() - 1);
160
161            return createUpdateResponse(memento, true);
162        } catch (final Exception e) {
163            checkForInsufficientStorageException(e, e);
164            return null; // not reachable
165        } finally {
166            transaction.releaseResourceLocksIfShortLived();
167        }
168    }
169
170    /**
171     * Get the list of versions for the object
172     *
173     * @param acceptValue the rdf media-type
174     * @return List of versions for the object as RDF
175     * @throws IOException in case of error extracting content
176     */
177    @GET
178    @HtmlTemplate(value = "fcr:versions")
179    @Produces({ TURTLE_WITH_CHARSET + ";qs=1.0", JSON_LD + ";qs=0.8",
180        N3_WITH_CHARSET, N3_ALT2_WITH_CHARSET, RDF_XML, NTRIPLES, TEXT_PLAIN_WITH_CHARSET,
181        TURTLE_X, TEXT_HTML_WITH_CHARSET, APPLICATION_LINK_FORMAT })
182    public Response getVersionList(@HeaderParam("Accept") final String acceptValue) throws IOException {
183
184        final FedoraResource theTimeMap = resource().getTimeMap();
185        checkCacheControlHeaders(request, servletResponse, theTimeMap, transaction());
186
187        LOGGER.debug("GET resource '{}'", externalPath());
188
189        addResourceHttpHeaders(theTimeMap);
190
191        if (acceptValue != null && acceptValue.equalsIgnoreCase(APPLICATION_LINK_FORMAT)) {
192            final String extUrl = identifierConverter().toDomain(externalPath());
193
194            final URI parentUri = URI.create(extUrl);
195            final List<Link> versionLinks = new ArrayList<>();
196            versionLinks.add(Link.fromUri(parentUri).rel("original").build());
197            versionLinks.add(Link.fromUri(parentUri).rel("timegate").build());
198            // So we don't collect the children twice, store them in an array.
199            final FedoraResource[] children = theTimeMap.getChildren().toArray(FedoraResource[]::new);
200
201            Arrays.stream(children).forEach(t -> {
202                final URI childUri = getUri(t);
203                versionLinks.add(Link.fromUri(childUri).rel("memento")
204                                     .param("datetime",
205                        MEMENTO_RFC_1123_FORMATTER.format(t.getMementoDatetime()))
206                                     .build());
207            });
208            // Based on the dates of the above mementos, add the range to the below link.
209            final Instant[] mementos = Arrays.stream(children).map(FedoraResource::getMementoDatetime)
210                .sorted(Comparator.naturalOrder())
211                .toArray(Instant[]::new);
212            final Builder linkBuilder =
213                Link.fromUri(parentUri + "/" + FCR_VERSIONS).rel("self").type(APPLICATION_LINK_FORMAT);
214            if (mementos.length >= 2) {
215                // There are 2 or more Mementos so make a range.
216                linkBuilder.param("from", MEMENTO_RFC_1123_FORMATTER.format(mementos[0].atZone(ZoneId.of("UTC"))));
217                linkBuilder.param("until",
218                    MEMENTO_RFC_1123_FORMATTER.format(mementos[mementos.length - 1].atZone(ZoneId.of("UTC"))));
219            }
220            versionLinks.add(linkBuilder.build());
221            return ok(new LinkFormatStream(versionLinks.stream())).build();
222        } else {
223            return getContent(getChildrenLimit(), theTimeMap);
224        }
225    }
226
227    /**
228     * Outputs information about the supported HTTP methods, etc.
229     *
230     * @return the information about the supported HTTP methods, etc.
231     */
232    @OPTIONS
233    public Response options() {
234        final FedoraResource theTimeMap = resource().getTimeMap();
235        LOGGER.info("OPTIONS for '{}'", externalPath);
236        addResourceHttpHeaders(theTimeMap);
237        return ok().build();
238    }
239
240    /**
241     * Can't delete TimeMaps
242     *
243     * @return the response to a delete request.
244     */
245    @DELETE
246    @Produces({TEXT_PLAIN_WITH_CHARSET})
247    public Response delete() {
248        final FedoraResource theTimeMap = resource().getTimeMap();
249        addResourceHttpHeaders(theTimeMap);
250        final String message = "Timemaps are deleted with their associated resource.";
251        return status(METHOD_NOT_ALLOWED).entity(message).type(TEXT_PLAIN_WITH_CHARSET).build();
252    }
253
254    @Override
255    protected String externalPath() {
256        return externalPath;
257    }
258}