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
020
021import static java.util.EnumSet.of;
022import static java.util.stream.Stream.concat;
023import static java.util.stream.Stream.empty;
024import static javax.ws.rs.core.HttpHeaders.CACHE_CONTROL;
025import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
026import static javax.ws.rs.core.HttpHeaders.CONTENT_DISPOSITION;
027import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
028import static javax.ws.rs.core.HttpHeaders.LINK;
029import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
030import static javax.ws.rs.core.Response.ok;
031import static javax.ws.rs.core.Response.status;
032import static javax.ws.rs.core.Response.temporaryRedirect;
033import static javax.ws.rs.core.Response.Status.PARTIAL_CONTENT;
034import static javax.ws.rs.core.Response.Status.REQUESTED_RANGE_NOT_SATISFIABLE;
035import static org.apache.commons.lang3.StringUtils.isBlank;
036import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel;
037import static org.apache.jena.rdf.model.ResourceFactory.createProperty;
038import static org.apache.jena.riot.RDFLanguages.contentTypeToLang;
039import static org.apache.jena.vocabulary.RDF.type;
040
041import static org.fcrepo.kernel.api.FedoraTypes.LDP_BASIC_CONTAINER;
042import static org.fcrepo.kernel.api.FedoraTypes.LDP_DIRECT_CONTAINER;
043import static org.fcrepo.kernel.api.FedoraTypes.LDP_INDIRECT_CONTAINER;
044import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER;
045import static org.fcrepo.kernel.api.RdfLexicon.CONTAINER;
046import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER;
047import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER;
048import static org.fcrepo.kernel.api.RdfLexicon.LDP_NAMESPACE;
049import static org.fcrepo.kernel.api.RdfLexicon.isManagedNamespace;
050import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate;
051import static org.fcrepo.kernel.api.RequiredRdfContext.EMBED_RESOURCES;
052import static org.fcrepo.kernel.api.RequiredRdfContext.INBOUND_REFERENCES;
053import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_CONTAINMENT;
054import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_MEMBERSHIP;
055import static org.fcrepo.kernel.api.RequiredRdfContext.MINIMAL;
056import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES;
057import static org.fcrepo.kernel.api.RequiredRdfContext.SERVER_MANAGED;
058
059
060import java.io.IOException;
061import java.io.InputStream;
062import java.net.URI;
063import java.net.URISyntaxException;
064import java.time.Instant;
065import java.util.ArrayList;
066import java.util.Collection;
067import java.util.Date;
068import java.util.HashSet;
069import java.util.List;
070import java.util.Set;
071import java.util.function.Predicate;
072import java.util.stream.Collectors;
073import java.util.stream.Stream;
074
075import javax.inject.Inject;
076import javax.servlet.http.HttpServletResponse;
077import javax.ws.rs.BadRequestException;
078import javax.ws.rs.BeanParam;
079import javax.ws.rs.WebApplicationException;
080import javax.ws.rs.core.CacheControl;
081import javax.ws.rs.core.Context;
082import javax.ws.rs.core.EntityTag;
083import javax.ws.rs.core.Link;
084import javax.ws.rs.core.MediaType;
085import javax.ws.rs.core.Request;
086import javax.ws.rs.core.Response;
087
088import org.apache.jena.atlas.RuntimeIOException;
089import org.apache.jena.graph.Triple;
090import org.apache.jena.rdf.model.Model;
091import org.apache.jena.riot.Lang;
092import org.apache.jena.riot.RiotException;
093import org.fcrepo.http.commons.api.HttpHeaderInjector;
094import org.fcrepo.http.commons.api.rdf.HttpTripleUtil;
095import org.fcrepo.http.commons.domain.MultiPrefer;
096import org.fcrepo.http.commons.domain.PreferTag;
097import org.fcrepo.http.commons.domain.Range;
098import org.fcrepo.http.commons.domain.ldp.LdpPreferTag;
099import org.fcrepo.http.commons.responses.RangeRequestInputStream;
100import org.fcrepo.http.commons.responses.RdfNamespacedStream;
101import org.fcrepo.http.commons.session.HttpSession;
102import org.fcrepo.kernel.api.RdfStream;
103import org.fcrepo.kernel.api.TripleCategory;
104import org.fcrepo.kernel.api.exception.InvalidChecksumException;
105import org.fcrepo.kernel.api.exception.MalformedRdfException;
106import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
107import org.fcrepo.kernel.api.models.Container;
108import org.fcrepo.kernel.api.models.FedoraBinary;
109import org.fcrepo.kernel.api.models.FedoraResource;
110import org.fcrepo.kernel.api.models.NonRdfSourceDescription;
111import org.fcrepo.kernel.api.rdf.DefaultRdfStream;
112import org.fcrepo.kernel.api.services.policy.StoragePolicyDecisionPoint;
113
114import org.glassfish.jersey.media.multipart.ContentDisposition;
115import org.jvnet.hk2.annotations.Optional;
116
117import com.fasterxml.jackson.core.JsonParseException;
118import com.google.common.annotations.VisibleForTesting;
119
120/**
121 * An abstract class that sits between AbstractResource and any resource that
122 * wishes to share the routines for building responses containing binary
123 * content.
124 *
125 * @author Mike Durbin
126 * @author ajs6f
127 */
128public abstract class ContentExposingResource extends FedoraBaseResource {
129
130    public static final MediaType MESSAGE_EXTERNAL_BODY = MediaType.valueOf("message/external-body");
131
132    @Context protected Request request;
133    @Context protected HttpServletResponse servletResponse;
134
135    @Inject
136    @Optional
137    private HttpTripleUtil httpTripleUtil;
138
139    @Inject
140    @Optional
141    private HttpHeaderInjector httpHeaderInject;
142
143    @BeanParam
144    protected MultiPrefer prefer;
145
146    @Inject
147    @Optional
148    StoragePolicyDecisionPoint storagePolicyDecisionPoint;
149
150    protected FedoraResource resource;
151
152    @Inject
153    protected  PathLockManager lockManager;
154
155    private static final Predicate<Triple> IS_MANAGED_TYPE = t -> t.getPredicate().equals(type.asNode()) &&
156            isManagedNamespace.test(t.getObject().getNameSpace());
157    private static final Predicate<Triple> IS_MANAGED_TRIPLE = IS_MANAGED_TYPE
158        .or(t -> isManagedPredicate.test(createProperty(t.getPredicate().getURI())));
159
160    protected abstract String externalPath();
161
162    protected Response getContent(final String rangeValue,
163                                  final RdfStream rdfStream) throws IOException {
164        return getContent(rangeValue, -1, rdfStream);
165    }
166
167    /**
168     * This method returns an HTTP response with content body appropriate to the following arguments.
169     *
170     * @param rangeValue starting and ending byte offsets, see {@link Range}
171     * @param limit is the number of child resources returned in the response, -1 for all
172     * @param rdfStream to which response RDF will be concatenated
173     * @return HTTP response
174     * @throws IOException in case of error extracting content
175     */
176    protected Response getContent(final String rangeValue,
177                                  final int limit,
178                                  final RdfStream rdfStream) throws IOException {
179
180        final RdfNamespacedStream outputStream;
181
182        if (resource() instanceof FedoraBinary) {
183
184            final MediaType mediaType = MediaType.valueOf(((FedoraBinary) resource()).getMimeType());
185
186            if (isExternalBody(mediaType)) {
187                return temporaryRedirect(URI.create(mediaType.getParameters().get("URL"))).build();
188            }
189
190            return getBinaryContent(rangeValue);
191        } else {
192            outputStream = new RdfNamespacedStream(
193                    new DefaultRdfStream(rdfStream.topic(), concat(rdfStream,
194                        getResourceTriples(limit))),
195                    session.getFedoraSession().getNamespaces());
196            if (prefer != null) {
197                prefer.getReturn().addResponseHeaders(servletResponse);
198            }
199        }
200        servletResponse.addHeader("Vary", "Accept, Range, Accept-Encoding, Accept-Language");
201
202        return ok(outputStream).build();
203    }
204
205    protected boolean isExternalBody(final MediaType mediaType) {
206        return MESSAGE_EXTERNAL_BODY.isCompatible(mediaType) &&
207                mediaType.getParameters().containsKey("access-type") &&
208                mediaType.getParameters().get("access-type").equals("URL") &&
209                mediaType.getParameters().containsKey("URL");
210    }
211
212    protected RdfStream getResourceTriples() {
213        return getResourceTriples(-1);
214    }
215
216    /**
217     * This method returns a stream of RDF triples associated with this target resource
218     *
219     * @param limit is the number of child resources returned in the response, -1 for all
220     * @return {@link RdfStream}
221     */
222    protected RdfStream getResourceTriples(final int limit) {
223        // use the thing described, not the description, for the subject of descriptive triples
224        if (resource() instanceof NonRdfSourceDescription) {
225            resource = resource().getDescribedResource();
226        }
227        final PreferTag returnPreference;
228
229        if (prefer != null && prefer.hasReturn()) {
230            returnPreference = prefer.getReturn();
231        } else if (prefer != null && prefer.hasHandling()) {
232            returnPreference = prefer.getHandling();
233        } else {
234            returnPreference = PreferTag.emptyTag();
235        }
236
237        final LdpPreferTag ldpPreferences = new LdpPreferTag(returnPreference);
238
239        final Predicate<Triple> tripleFilter = ldpPreferences.prefersServerManaged() ? x -> true :
240            IS_MANAGED_TRIPLE.negate();
241
242        final List<Stream<Triple>> streams = new ArrayList<>();
243
244
245        if (returnPreference.getValue().equals("minimal")) {
246            streams.add(getTriples(of(PROPERTIES, MINIMAL)).filter(tripleFilter));
247
248            if (ldpPreferences.prefersServerManaged()) {
249                streams.add(getTriples(of(SERVER_MANAGED, MINIMAL)));
250            }
251        } else {
252            streams.add(getTriples(PROPERTIES).filter(tripleFilter));
253
254            // Additional server-managed triples about this resource
255            if (ldpPreferences.prefersServerManaged()) {
256                streams.add(getTriples(SERVER_MANAGED));
257            }
258
259            // containment triples about this resource
260            if (ldpPreferences.prefersContainment()) {
261                if (limit == -1) {
262                    streams.add(getTriples(LDP_CONTAINMENT));
263                } else {
264                    streams.add(getTriples(LDP_CONTAINMENT).limit(limit));
265                }
266            }
267
268            // LDP container membership triples for this resource
269            if (ldpPreferences.prefersMembership()) {
270                streams.add(getTriples(LDP_MEMBERSHIP));
271            }
272
273            // Include inbound references to this object
274            if (ldpPreferences.prefersReferences()) {
275                streams.add(getTriples(INBOUND_REFERENCES));
276            }
277
278            // Embed the children of this object
279            if (ldpPreferences.prefersEmbed()) {
280                streams.add(getTriples(EMBED_RESOURCES));
281            }
282        }
283
284        final RdfStream rdfStream = new DefaultRdfStream(
285                asNode(resource()), streams.stream().reduce(empty(), Stream::concat));
286
287        if (httpTripleUtil != null && ldpPreferences.prefersServerManaged()) {
288            return httpTripleUtil.addHttpComponentModelsForResourceToStream(rdfStream, resource(), uriInfo,
289                    translator());
290        }
291
292        return rdfStream;
293    }
294
295    /**
296     * Get the binary content of a datastream
297     *
298     * @param rangeValue the range value
299     * @return Binary blob
300     * @throws IOException if io exception occurred
301     */
302    protected Response getBinaryContent(final String rangeValue)
303            throws IOException {
304            final FedoraBinary binary = (FedoraBinary)resource();
305
306            // we include an explicit etag, because the default behavior is to use the JCR node's etag, not
307            // the jcr:content node digest. The etag is only included if we are not within a transaction.
308            if (!session().isBatchSession()) {
309                checkCacheControlHeaders(request, servletResponse, binary, session());
310            }
311            final CacheControl cc = new CacheControl();
312            cc.setMaxAge(0);
313            cc.setMustRevalidate(true);
314            Response.ResponseBuilder builder;
315
316            if (rangeValue != null && rangeValue.startsWith("bytes")) {
317
318                final Range range = Range.convert(rangeValue);
319
320                final long contentSize = binary.getContentSize();
321
322                final String endAsString;
323
324                if (range.end() == -1) {
325                    endAsString = Long.toString(contentSize - 1);
326                } else {
327                    endAsString = Long.toString(range.end());
328                }
329
330                final String contentRangeValue =
331                        String.format("bytes %s-%s/%s", range.start(),
332                                endAsString, contentSize);
333
334                if (range.end() > contentSize ||
335                        (range.end() == -1 && range.start() > contentSize)) {
336
337                    builder = status(REQUESTED_RANGE_NOT_SATISFIABLE)
338                            .header("Content-Range", contentRangeValue);
339                } else {
340                    @SuppressWarnings("resource")
341                    final RangeRequestInputStream rangeInputStream =
342                            new RangeRequestInputStream(binary.getContent(), range.start(), range.size());
343
344                    builder = status(PARTIAL_CONTENT).entity(rangeInputStream)
345                            .header("Content-Range", contentRangeValue);
346                }
347
348            } else {
349                @SuppressWarnings("resource")
350                final InputStream content = binary.getContent();
351                builder = ok(content);
352            }
353
354
355            // we set the content-type explicitly to avoid content-negotiation from getting in the way
356            return builder.type(binary.getMimeType())
357                    .cacheControl(cc)
358                    .build();
359
360        }
361
362    protected RdfStream getTriples(final Set<? extends TripleCategory> x) {
363        return getTriples(resource(), x);
364    }
365
366    protected RdfStream getTriples(final FedoraResource resource, final Set<? extends TripleCategory> x) {
367        return resource.getTriples(translator(), x);
368    }
369
370    protected RdfStream getTriples(final TripleCategory x) {
371        return getTriples(resource(), x);
372    }
373
374    protected RdfStream getTriples(final FedoraResource resource, final TripleCategory x) {
375        return resource.getTriples(translator(), x);
376    }
377
378    protected URI getUri(final FedoraResource resource) {
379        try {
380            final String uri = translator().reverse().convert(resource).getURI();
381            return new URI(uri);
382        } catch (final URISyntaxException e) {
383            throw new BadRequestException(e);
384        }
385    }
386
387    protected FedoraResource resource() {
388        if (resource == null) {
389            resource = getResourceFromPath(externalPath());
390        }
391        return resource;
392    }
393
394    protected void addResourceLinkHeaders(final FedoraResource resource) {
395        addResourceLinkHeaders(resource, false);
396    }
397
398    protected void addResourceLinkHeaders(final FedoraResource resource, final boolean includeAnchor) {
399        if (resource instanceof NonRdfSourceDescription) {
400            final URI uri = getUri(resource.getDescribedResource());
401            final Link link = Link.fromUri(uri).rel("describes").build();
402            servletResponse.addHeader(LINK, link.toString());
403        } else if (resource instanceof FedoraBinary) {
404            final URI uri = getUri(resource.getDescription());
405            final Link.Builder builder = Link.fromUri(uri).rel("describedby");
406
407            if (includeAnchor) {
408                builder.param("anchor", getUri(resource).toString());
409            }
410            servletResponse.addHeader(LINK, builder.build().toString());
411        }
412    }
413
414    /**
415     * Add any resource-specific headers to the response
416     * @param resource the resource
417     */
418    protected void addResourceHttpHeaders(final FedoraResource resource) {
419        if (resource instanceof FedoraBinary) {
420            final FedoraBinary binary = (FedoraBinary)resource;
421            final Date createdDate = binary.getCreatedDate() != null ? Date.from(binary.getCreatedDate()) : null;
422            final Date modDate = binary.getLastModifiedDate() != null ? Date.from(binary.getLastModifiedDate()) : null;
423
424            final ContentDisposition contentDisposition = ContentDisposition.type("attachment")
425                    .fileName(binary.getFilename())
426                    .creationDate(createdDate)
427                    .modificationDate(modDate)
428                    .size(binary.getContentSize())
429                    .build();
430
431            servletResponse.addHeader(CONTENT_TYPE, binary.getMimeType());
432            servletResponse.addHeader(CONTENT_LENGTH, String.valueOf(binary.getContentSize()));
433            servletResponse.addHeader("Accept-Ranges", "bytes");
434            servletResponse.addHeader(CONTENT_DISPOSITION, contentDisposition.toString());
435        }
436
437        servletResponse.addHeader(LINK, "<" + LDP_NAMESPACE + "Resource>;rel=\"type\"");
438
439        if (resource instanceof FedoraBinary) {
440            servletResponse.addHeader(LINK, "<" + LDP_NAMESPACE + "NonRDFSource>;rel=\"type\"");
441        } else if (resource instanceof Container) {
442            servletResponse.addHeader(LINK, "<" + CONTAINER.getURI() + ">;rel=\"type\"");
443            if (resource.hasType(LDP_BASIC_CONTAINER)) {
444                servletResponse.addHeader(LINK, "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\"");
445            } else if (resource.hasType(LDP_DIRECT_CONTAINER)) {
446                servletResponse.addHeader(LINK, "<" + DIRECT_CONTAINER.getURI() + ">;rel=\"type\"");
447            } else if (resource.hasType(LDP_INDIRECT_CONTAINER)) {
448                servletResponse.addHeader(LINK, "<" + INDIRECT_CONTAINER.getURI() + ">;rel=\"type\"");
449            } else {
450                servletResponse.addHeader(LINK, "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\"");
451            }
452        } else {
453            servletResponse.addHeader(LINK, "<" + LDP_NAMESPACE + "RDFSource>;rel=\"type\"");
454        }
455        if (httpHeaderInject != null) {
456            httpHeaderInject.addHttpHeaderToResponseStream(servletResponse, uriInfo, resource());
457        }
458
459    }
460
461    /**
462     * Evaluate the cache control headers for the request to see if it can be served from
463     * the cache.
464     *
465     * @param request the request
466     * @param servletResponse the servlet response
467     * @param resource the fedora resource
468     * @param session the session
469     */
470    protected void checkCacheControlHeaders(final Request request,
471                                                   final HttpServletResponse servletResponse,
472                                                   final FedoraResource resource,
473                                                   final HttpSession session) {
474        evaluateRequestPreconditions(request, servletResponse, resource, session, true);
475        addCacheControlHeaders(servletResponse, resource, session);
476    }
477
478    /**
479     * Add ETag and Last-Modified cache control headers to the response
480     * <p>
481     * Note: In this implementation, the HTTP headers for ETags and Last-Modified dates are swapped
482     * for fedora:Binary resources and their descriptions. Here, we are drawing a distinction between
483     * the HTTP resource and the LDP resource. As an HTTP resource, the last-modified header should
484     * reflect when the resource at the given URL was last changed. With fedora:Binary resources and
485     * their descriptions, this is a little complicated, for the descriptions have, as their subjects,
486     * the binary itself. And the fedora:lastModified property produced by that NonRdfSourceDescription
487     * refers to the last-modified date of the binary -- not the last-modified date of the
488     * NonRdfSourceDescription.
489     * </p>
490     * @param servletResponse the servlet response
491     * @param resource the fedora resource
492     * @param session the session
493     */
494    protected void addCacheControlHeaders(final HttpServletResponse servletResponse,
495                                                 final FedoraResource resource,
496                                                 final HttpSession session) {
497
498        if (session.isBatchSession()) {
499            // Do not add caching headers if in a transaction
500            return;
501        }
502
503        final EntityTag etag;
504        final Instant date;
505
506        // See note about this code in the javadoc above.
507        if (resource instanceof FedoraBinary) {
508            // Use a strong ETag for LDP-NR
509            etag = new EntityTag(resource.getDescription().getEtagValue());
510            date = resource.getDescription().getLastModifiedDate();
511        } else {
512            // Use a weak ETag for the LDP-RS
513            etag = new EntityTag(resource.getDescribedResource().getEtagValue(), true);
514            date = resource.getDescribedResource().getLastModifiedDate();
515        }
516
517        if (!etag.getValue().isEmpty()) {
518            servletResponse.addHeader("ETag", etag.toString());
519        }
520
521        if (date != null) {
522            servletResponse.addDateHeader("Last-Modified", date.toEpochMilli());
523        }
524    }
525
526    /**
527     * Evaluate request preconditions to ensure the resource is the expected state
528     * @param request the request
529     * @param servletResponse the servlet response
530     * @param resource the resource
531     * @param session the session
532     */
533    protected void evaluateRequestPreconditions(final Request request,
534                                                       final HttpServletResponse servletResponse,
535                                                       final FedoraResource resource,
536                                                       final HttpSession session) {
537        evaluateRequestPreconditions(request, servletResponse, resource, session, false);
538    }
539
540    @VisibleForTesting
541    void evaluateRequestPreconditions(final Request request,
542                                                     final HttpServletResponse servletResponse,
543                                                     final FedoraResource resource,
544                                                     final HttpSession session,
545                                                     final boolean cacheControl) {
546
547        if (session.isBatchSession()) {
548            // Force cache revalidation if in a transaction
549            servletResponse.addHeader(CACHE_CONTROL, "must-revalidate");
550            servletResponse.addHeader(CACHE_CONTROL, "max-age=0");
551            return;
552        }
553
554        final EntityTag etag;
555        final Instant date;
556        Instant roundedDate = Instant.now();
557
558        // See the related note about the next block of code in the
559        // ContentExposingResource::addCacheControlHeaders method
560        if (resource instanceof FedoraBinary) {
561            // Use a strong ETag for the LDP-NR
562            etag = new EntityTag(resource.getDescription().getEtagValue());
563            date = resource.getDescription().getLastModifiedDate();
564        } else {
565            // Use a strong ETag for the LDP-RS when validating If-(None)-Match headers
566            etag = new EntityTag(resource.getDescribedResource().getEtagValue());
567            date = resource.getDescribedResource().getLastModifiedDate();
568        }
569
570        if (date != null) {
571            roundedDate = date.minusMillis(date.toEpochMilli() % 1000);
572        }
573
574        Response.ResponseBuilder builder = request.evaluatePreconditions(etag);
575        if ( builder == null ) {
576            builder = request.evaluatePreconditions(Date.from(roundedDate));
577        }
578
579        if (builder != null && cacheControl ) {
580            final CacheControl cc = new CacheControl();
581            cc.setMaxAge(0);
582            cc.setMustRevalidate(true);
583            // here we are implicitly emitting a 304
584            // the exception is not an error, it's genuinely
585            // an exceptional condition
586            builder = builder.cacheControl(cc).lastModified(Date.from(roundedDate)).tag(etag);
587        }
588        if (builder != null) {
589            throw new WebApplicationException(builder.build());
590        }
591    }
592
593    protected static MediaType getSimpleContentType(final MediaType requestContentType) {
594        return requestContentType != null ? new MediaType(requestContentType.getType(), requestContentType.getSubtype())
595                : APPLICATION_OCTET_STREAM_TYPE;
596    }
597
598    protected static boolean isRdfContentType(final String contentTypeString) {
599        return contentTypeToLang(contentTypeString) != null;
600    }
601
602    protected void replaceResourceBinaryWithStream(final FedoraBinary result,
603                                                   final InputStream requestBodyStream,
604                                                   final ContentDisposition contentDisposition,
605                                                   final MediaType contentType,
606                                                   final Collection<String> checksums) throws InvalidChecksumException {
607        final Collection<URI> checksumURIs = checksums == null ?
608                new HashSet<>() : checksums.stream().map(checksum -> checksumURI(checksum)).collect(Collectors.toSet());
609        final String originalFileName = contentDisposition != null ? contentDisposition.getFileName() : "";
610        final String originalContentType = contentType != null ? contentType.toString() : "";
611
612        result.setContent(requestBodyStream,
613                originalContentType,
614                checksumURIs,
615                originalFileName,
616                storagePolicyDecisionPoint);
617    }
618
619    protected void replaceResourceWithStream(final FedoraResource resource,
620                                             final InputStream requestBodyStream,
621                                             final MediaType contentType,
622                                             final RdfStream resourceTriples) throws MalformedRdfException {
623        final Lang format = contentTypeToLang(contentType.toString());
624
625        final Model inputModel = createDefaultModel();
626        try {
627            inputModel.read(requestBodyStream, getUri(resource).toString(), format.getName().toUpperCase());
628
629        } catch (final RiotException e) {
630            throw new BadRequestException("RDF was not parsable: " + e.getMessage(), e);
631
632        } catch (final RuntimeIOException e) {
633            if (e.getCause() instanceof JsonParseException) {
634                throw new MalformedRdfException(e.getCause());
635            }
636            throw new RepositoryRuntimeException(e);
637        }
638
639        resource.replaceProperties(translator(), inputModel, resourceTriples);
640    }
641
642    protected void patchResourcewithSparql(final FedoraResource resource,
643            final String requestBody,
644            final RdfStream resourceTriples) {
645        resource.getDescribedResource().updateProperties(translator(), requestBody, resourceTriples);
646    }
647
648    /**
649     * Create a checksum URI object.
650     **/
651    private static URI checksumURI( final String checksum ) {
652        if (!isBlank(checksum)) {
653            return URI.create(checksum);
654        }
655        return null;
656    }
657}