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