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