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 static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; 021import static javax.ws.rs.core.HttpHeaders.LINK; 022import static javax.ws.rs.core.Response.Status.CONFLICT; 023import static javax.ws.rs.core.Response.Status.UNSUPPORTED_MEDIA_TYPE; 024import static javax.ws.rs.core.Response.ok; 025import static org.apache.commons.lang3.StringUtils.isBlank; 026import static org.apache.jena.riot.RDFLanguages.contentTypeToLang; 027import static org.fcrepo.http.commons.domain.RDFMediaType.APPLICATION_LINK_FORMAT; 028import static org.fcrepo.http.commons.domain.RDFMediaType.JSON_LD; 029import static org.fcrepo.http.commons.domain.RDFMediaType.N3_ALT2_WITH_CHARSET; 030import static org.fcrepo.http.commons.domain.RDFMediaType.N3_WITH_CHARSET; 031import static org.fcrepo.http.commons.domain.RDFMediaType.NTRIPLES; 032import static org.fcrepo.http.commons.domain.RDFMediaType.RDF_XML; 033import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_HTML_WITH_CHARSET; 034import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_PLAIN_WITH_CHARSET; 035import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_WITH_CHARSET; 036import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_X; 037import static org.fcrepo.kernel.api.FedoraTypes.FCR_VERSIONS; 038import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_RFC_1123_FORMATTER; 039import static org.slf4j.LoggerFactory.getLogger; 040 041import java.io.IOException; 042import java.io.InputStream; 043import java.net.URI; 044import java.time.Instant; 045import java.time.ZoneId; 046import java.time.format.DateTimeParseException; 047import java.util.ArrayList; 048import java.util.Arrays; 049import java.util.Collection; 050import java.util.Comparator; 051import java.util.HashSet; 052import java.util.List; 053import java.util.stream.Collectors; 054 055import javax.jcr.ItemExistsException; 056import javax.servlet.http.HttpServletResponse; 057import javax.ws.rs.BadRequestException; 058import javax.ws.rs.ClientErrorException; 059import javax.ws.rs.GET; 060import javax.ws.rs.HeaderParam; 061import javax.ws.rs.OPTIONS; 062import javax.ws.rs.POST; 063import javax.ws.rs.Path; 064import javax.ws.rs.PathParam; 065import javax.ws.rs.Produces; 066import javax.ws.rs.core.Context; 067import javax.ws.rs.core.Link; 068import javax.ws.rs.core.Link.Builder; 069import javax.ws.rs.core.MediaType; 070import javax.ws.rs.core.Request; 071import javax.ws.rs.core.Response; 072import javax.ws.rs.core.UriInfo; 073import com.google.common.annotations.VisibleForTesting; 074import org.apache.jena.riot.Lang; 075import org.fcrepo.http.api.PathLockManager.AcquiredLock; 076import org.fcrepo.http.commons.responses.HtmlTemplate; 077import org.fcrepo.http.commons.responses.LinkFormatStream; 078import org.fcrepo.kernel.api.RdfStream; 079import org.fcrepo.kernel.api.exception.InvalidChecksumException; 080import org.fcrepo.kernel.api.exception.MementoDatetimeFormatException; 081import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 082import org.fcrepo.kernel.api.exception.UnsupportedAlgorithmException; 083import org.fcrepo.kernel.api.models.FedoraBinary; 084import org.fcrepo.kernel.api.models.FedoraResource; 085import org.fcrepo.kernel.api.rdf.DefaultRdfStream; 086import org.slf4j.Logger; 087import org.springframework.context.annotation.Scope; 088 089/** 090 * @author cabeer 091 * @since 9/25/14 092 */ 093@Scope("request") 094@Path("/{path: .*}/fcr:versions") 095public class FedoraVersioning extends ContentExposingResource { 096 097 private static final Logger LOGGER = getLogger(FedoraVersioning.class); 098 099 @VisibleForTesting 100 public static final String MEMENTO_DATETIME_HEADER = "Memento-Datetime"; 101 102 @Context protected Request request; 103 @Context protected HttpServletResponse servletResponse; 104 @Context protected UriInfo uriInfo; 105 106 @PathParam("path") protected String externalPath; 107 108 109 /** 110 * Default JAX-RS entry point 111 */ 112 public FedoraVersioning() { 113 super(); 114 } 115 116 /** 117 * Create a new FedoraNodes instance for a given path 118 * @param externalPath the external path 119 */ 120 @VisibleForTesting 121 public FedoraVersioning(final String externalPath) { 122 this.externalPath = externalPath; 123 } 124 125 /** 126 * Create a new version of a resource. If a memento-datetime header is provided, then the new version will be 127 * based off the provided body using that datetime. If one was not provided, then a version is created based off 128 * the current version of the resource. 129 * 130 * @param datetimeHeader memento-datetime header 131 * @param requestContentType Content-Type of the request body 132 * @param digest digests of the request body 133 * @param requestBodyStream request body stream 134 * @return response 135 * @throws InvalidChecksumException thrown if one of the provided digests does not match the content 136 * @throws MementoDatetimeFormatException if the header value of memento-datetime is not RFC-1123 format 137 */ 138 @POST 139 public Response addVersion(@HeaderParam(MEMENTO_DATETIME_HEADER) final String datetimeHeader, 140 @HeaderParam(CONTENT_TYPE) final MediaType requestContentType, 141 @HeaderParam("Digest") final String digest, 142 final InputStream requestBodyStream, 143 @HeaderParam(LINK) final List<String> rawLinks) 144 throws InvalidChecksumException, MementoDatetimeFormatException { 145 146 final FedoraResource resource = resource(); 147 final FedoraResource timeMap = resource.getTimeMap(); 148 149 final AcquiredLock lock = lockManager.lockForWrite(timeMap.getPath(), 150 session.getFedoraSession(), nodeService); 151 152 try { 153 final MediaType contentType = getSimpleContentType(requestContentType); 154 155 final String slug = headers.getHeaderString("Slug"); 156 if (slug != null) { 157 throw new BadRequestException("Slug header is no longer supported for versioning label. " 158 + "Please use " + MEMENTO_DATETIME_HEADER + " header with RFC-1123 date-time."); 159 } 160 161 final Instant mementoInstant; 162 try { 163 mementoInstant = (isBlank(datetimeHeader) ? Instant.now() 164 : Instant.from(MEMENTO_RFC_1123_FORMATTER.parse(datetimeHeader))); 165 } catch (final DateTimeParseException e) { 166 throw new MementoDatetimeFormatException("Invalid memento date-time value. " 167 + "Please use RFC-1123 date-time format, such as 'Tue, 3 Jun 2008 11:05:30 GMT'", e); 168 } 169 170 final boolean createFromExisting = isBlank(datetimeHeader); 171 172 try { 173 LOGGER.debug("Request to add version for date '{}' for '{}'", datetimeHeader, externalPath); 174 175 // Create memento 176 FedoraResource memento = null; 177 final boolean isBinary = resource instanceof FedoraBinary; 178 if (isBinary) { 179 final FedoraBinary binaryResource = (FedoraBinary) resource; 180 if (createFromExisting) { 181 memento = versionService.createBinaryVersion(session.getFedoraSession(), 182 binaryResource, mementoInstant, storagePolicyDecisionPoint); 183 } else { 184 final List<String> links = unpackLinks(rawLinks); 185 final ExternalContentHandler extContent = extContentHandlerFactory.createFromLinks(links); 186 187 memento = createBinaryMementoFromRequest(binaryResource, mementoInstant, 188 requestBodyStream, extContent, digest); 189 } 190 } 191 // Create rdf memento if the request resource was an rdf resource or a binary from the 192 // current version of the original resource. 193 if (!isBinary || createFromExisting) { 194 // Version the description in case the original is a binary 195 final FedoraResource originalResource = resource().getDescription(); 196 final InputStream bodyStream = createFromExisting ? null : requestBodyStream; 197 final Lang format = createFromExisting ? null : contentTypeToLang(contentType.toString()); 198 if (!createFromExisting && format == null) { 199 throw new ClientErrorException("Invalid Content Type " + contentType.toString(), 200 UNSUPPORTED_MEDIA_TYPE); 201 } 202 203 final FedoraResource rdfMemento = versionService.createVersion(session.getFedoraSession(), 204 originalResource, idTranslator, mementoInstant, bodyStream, format); 205 // If a binary memento was also generated, use the binary in the response 206 if (!isBinary) { 207 memento = rdfMemento; 208 } 209 } 210 211 session.commit(); 212 return createUpdateResponse(memento, true); 213 } catch (final Exception e) { 214 checkForInsufficientStorageException(e, e); 215 return null; // not reachable 216 } 217 } catch (final RepositoryRuntimeException e) { 218 if (e.getCause() instanceof ItemExistsException) { 219 throw new ClientErrorException("Memento with provided datetime already exists", 220 CONFLICT); 221 } else { 222 throw e; 223 } 224 } finally { 225 lock.release(); 226 } 227 } 228 229 private FedoraBinary createBinaryMementoFromRequest(final FedoraBinary binaryResource, 230 final Instant mementoInstant, 231 final InputStream requestBodyStream, 232 final ExternalContentHandler extContent, 233 final String digest) throws InvalidChecksumException, UnsupportedAlgorithmException { 234 235 final Collection<String> checksums = parseDigestHeader(digest); 236 final Collection<URI> checksumURIs = checksums == null ? new HashSet<>() : checksums.stream().map( 237 checksum -> checksumURI(checksum)).collect(Collectors.toSet()); 238 239 // Create internal binary either from supplied body or copy external uri 240 if (extContent == null || extContent.isCopy()) { 241 InputStream contentStream = requestBodyStream; 242 if (extContent != null) { 243 contentStream = extContent.fetchExternalContent(); 244 } 245 246 return versionService.createBinaryVersion(session.getFedoraSession(), binaryResource, 247 mementoInstant, contentStream, checksumURIs, storagePolicyDecisionPoint); 248 } else { 249 return versionService.createExternalBinaryVersion(session.getFedoraSession(), binaryResource, 250 mementoInstant, checksumURIs, extContent.getHandling(), extContent.getURL()); 251 } 252 } 253 254 /** 255 * Get the list of versions for the object 256 * 257 * @param rangeValue starting and ending byte offsets 258 * @param acceptValue the rdf media-type 259 * @return List of versions for the object as RDF 260 * @throws IOException in case of error extracting content 261 */ 262 @GET 263 @HtmlTemplate(value = "fcr:versions") 264 @Produces({ TURTLE_WITH_CHARSET + ";qs=1.0", JSON_LD + ";qs=0.8", 265 N3_WITH_CHARSET, N3_ALT2_WITH_CHARSET, RDF_XML, NTRIPLES, TEXT_PLAIN_WITH_CHARSET, 266 TURTLE_X, TEXT_HTML_WITH_CHARSET, APPLICATION_LINK_FORMAT }) 267 public Response getVersionList(@HeaderParam("Range") final String rangeValue, 268 @HeaderParam("Accept") final String acceptValue) throws IOException { 269 270 final FedoraResource theTimeMap = resource().getTimeMap(); 271 checkCacheControlHeaders(request, servletResponse, theTimeMap, session); 272 273 LOGGER.debug("GET resource '{}'", externalPath); 274 275 addResourceHttpHeaders(theTimeMap); 276 277 if (acceptValue != null && acceptValue.equalsIgnoreCase(APPLICATION_LINK_FORMAT)) { 278 final URI parentUri = getUri(resource()); 279 final List<Link> versionLinks = new ArrayList<>(); 280 versionLinks.add(Link.fromUri(parentUri).rel("original").build()); 281 versionLinks.add(Link.fromUri(parentUri).rel("timegate").build()); 282 // So we don't collect the children twice, store them in an array. 283 final FedoraResource[] children = theTimeMap.getChildren().toArray(FedoraResource[]::new); 284 285 Arrays.stream(children).forEach(t -> { 286 final URI childUri = getUri(t); 287 versionLinks.add(Link.fromUri(childUri).rel("memento") 288 .param("datetime", 289 MEMENTO_RFC_1123_FORMATTER.format(t.getMementoDatetime())) 290 .build()); 291 }); 292 // Based on the dates of the above mementos, add the range to the below link. 293 final Instant[] Mementos = Arrays.stream(children).map(FedoraResource::getMementoDatetime) 294 .sorted(Comparator.naturalOrder()) 295 .toArray(Instant[]::new); 296 final Builder linkBuilder = 297 Link.fromUri(parentUri + "/" + FCR_VERSIONS).rel("self").type(APPLICATION_LINK_FORMAT); 298 if (Mementos.length >= 2) { 299 // There are 2 or more Mementos so make a range. 300 linkBuilder.param("from", MEMENTO_RFC_1123_FORMATTER.format(Mementos[0].atZone(ZoneId.of("UTC")))); 301 linkBuilder.param("until", 302 MEMENTO_RFC_1123_FORMATTER.format(Mementos[Mementos.length - 1].atZone(ZoneId.of("UTC")))); 303 } 304 versionLinks.add(linkBuilder.build()); 305 return ok(new LinkFormatStream(versionLinks.stream())).build(); 306 } else { 307 final AcquiredLock readLock = lockManager.lockForRead(theTimeMap.getPath()); 308 try (final RdfStream rdfStream = new DefaultRdfStream(asNode(theTimeMap))) { 309 return getContent(rangeValue, getChildrenLimit(), rdfStream, theTimeMap); 310 } finally { 311 readLock.release(); 312 } 313 } 314 } 315 316 /** 317 * Outputs information about the supported HTTP methods, etc. 318 * 319 * @return the information about the supported HTTP methods, etc. 320 */ 321 @OPTIONS 322 public Response options() { 323 final FedoraResource theTimeMap = resource().getTimeMap(); 324 LOGGER.info("OPTIONS for '{}'", externalPath); 325 addResourceHttpHeaders(theTimeMap); 326 return ok().build(); 327 } 328 329 @Override 330 protected String externalPath() { 331 return externalPath; 332 } 333}