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}