001/*
002 * Copyright 2015 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.http.api;
017
018
019import static com.hp.hpl.jena.rdf.model.ModelFactory.createDefaultModel;
020import static com.hp.hpl.jena.vocabulary.RDF.type;
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.MediaType.APPLICATION_OCTET_STREAM_TYPE;
026import static javax.ws.rs.core.Response.ok;
027import static javax.ws.rs.core.Response.status;
028import static javax.ws.rs.core.Response.temporaryRedirect;
029import static javax.ws.rs.core.Response.Status.PARTIAL_CONTENT;
030import static javax.ws.rs.core.Response.Status.REQUESTED_RANGE_NOT_SATISFIABLE;
031import static org.apache.commons.lang3.StringUtils.isBlank;
032import static org.apache.jena.riot.RDFLanguages.contentTypeToLang;
033
034import static org.fcrepo.kernel.api.FedoraTypes.LDP_BASIC_CONTAINER;
035import static org.fcrepo.kernel.api.FedoraTypes.LDP_DIRECT_CONTAINER;
036import static org.fcrepo.kernel.api.FedoraTypes.LDP_INDIRECT_CONTAINER;
037import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER;
038import static org.fcrepo.kernel.api.RdfLexicon.CONTAINER;
039import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER;
040import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER;
041import static org.fcrepo.kernel.api.RdfLexicon.LDP_NAMESPACE;
042import static org.fcrepo.kernel.api.RdfLexicon.isManagedNamespace;
043import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicateURI;
044import static org.fcrepo.kernel.api.RequiredRdfContext.EMBED_RESOURCES;
045import static org.fcrepo.kernel.api.RequiredRdfContext.INBOUND_REFERENCES;
046import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_CONTAINMENT;
047import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_MEMBERSHIP;
048import static org.fcrepo.kernel.api.RequiredRdfContext.MINIMAL;
049import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES;
050import static org.fcrepo.kernel.api.RequiredRdfContext.SERVER_MANAGED;
051import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.getNamespaces;
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 com.fasterxml.jackson.core.JsonParseException;
079import org.apache.jena.atlas.RuntimeIOException;
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.exception.InvalidChecksumException;
090import org.fcrepo.kernel.api.exception.MalformedRdfException;
091import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
092import org.fcrepo.kernel.api.models.Container;
093import org.fcrepo.kernel.api.models.FedoraBinary;
094import org.fcrepo.kernel.api.models.FedoraResource;
095import org.fcrepo.kernel.api.models.NonRdfSource;
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 String contentTypeString = ((FedoraBinary) resource()).getMimeType();
171
172            final Lang lang = contentTypeToLang(contentTypeString);
173
174            if (!contentTypeString.equals("text/plain") && lang != null) {
175
176                final String format = lang.getName().toUpperCase();
177
178                try (final InputStream content = ((FedoraBinary) resource()).getContent()) {
179
180                    final Model inputModel = createDefaultModel()
181                            .read(content, (resource()).toString(), format);
182                    outputStream = new RdfNamespacedStream(
183                            new DefaultRdfStream(rdfStream.topic(), concat(rdfStream,
184                                    DefaultRdfStream.fromModel(rdfStream.topic(), inputModel))),
185                            getNamespaces(session()));
186                }
187
188
189            } else {
190
191                final MediaType mediaType = MediaType.valueOf(contentTypeString);
192                if (MESSAGE_EXTERNAL_BODY.isCompatible(mediaType)
193                        && mediaType.getParameters().containsKey("access-type")
194                        && mediaType.getParameters().get("access-type").equals("URL")
195                        && mediaType.getParameters().containsKey("URL") ) {
196                    try {
197                        return temporaryRedirect(new URI(mediaType.getParameters().get("URL"))).build();
198                    } catch (final URISyntaxException e) {
199                        throw new RepositoryRuntimeException(e);
200                    }
201                }
202                return getBinaryContent(rangeValue);
203            }
204
205        } else {
206            outputStream = new RdfNamespacedStream(
207                    new DefaultRdfStream(rdfStream.topic(), concat(rdfStream,
208                        getResourceTriples(limit))),
209                    getNamespaces(session()));
210            if (prefer != null) {
211                prefer.getReturn().addResponseHeaders(servletResponse);
212            }
213        }
214        servletResponse.addHeader("Vary", "Accept, Range, Accept-Encoding, Accept-Language");
215
216        return ok(outputStream).build();
217    }
218
219    protected RdfStream getResourceTriples() {
220        return getResourceTriples(-1);
221    }
222
223    /**
224     * This method returns a stream of RDF triples associated with this target resource
225     *
226     * @param limit is the number of child resources returned in the response, -1 for all
227     * @return {@link RdfStream}
228     */
229    protected RdfStream getResourceTriples(final int limit) {
230        // use the thing described, not the description, for the subject of descriptive triples
231        if (resource() instanceof NonRdfSourceDescription) {
232            resource = ((NonRdfSourceDescription) resource()).getDescribedResource();
233        }
234        final PreferTag returnPreference;
235
236        if (prefer != null && prefer.hasReturn()) {
237            returnPreference = prefer.getReturn();
238        } else if (prefer != null && prefer.hasHandling()) {
239            returnPreference = prefer.getHandling();
240        } else {
241            returnPreference = PreferTag.emptyTag();
242        }
243
244        final LdpPreferTag ldpPreferences = new LdpPreferTag(returnPreference);
245
246        final Predicate<Triple> tripleFilter = ldpPreferences.prefersServerManaged() ? x -> true :
247            IS_MANAGED_TRIPLE.negate();
248
249        final List<Stream<Triple>> streams = new ArrayList<>();
250
251
252        if (returnPreference.getValue().equals("minimal")) {
253            streams.add(getTriples(of(PROPERTIES, MINIMAL)).filter(tripleFilter));
254
255            if (ldpPreferences.prefersServerManaged()) {
256                streams.add(getTriples(of(SERVER_MANAGED, MINIMAL)));
257            }
258        } else {
259            streams.add(getTriples(PROPERTIES).filter(tripleFilter));
260
261            // Additional server-managed triples about this resource
262            if (ldpPreferences.prefersServerManaged()) {
263                streams.add(getTriples(SERVER_MANAGED));
264            }
265
266            // containment triples about this resource
267            if (ldpPreferences.prefersContainment()) {
268                if (limit == -1) {
269                    streams.add(getTriples(LDP_CONTAINMENT));
270                } else {
271                    streams.add(getTriples(LDP_CONTAINMENT).limit(limit));
272                }
273            }
274
275            // LDP container membership triples for this resource
276            if (ldpPreferences.prefersMembership()) {
277                streams.add(getTriples(LDP_MEMBERSHIP));
278            }
279
280            // Include inbound references to this object
281            if (ldpPreferences.prefersReferences()) {
282                streams.add(getTriples(INBOUND_REFERENCES));
283            }
284
285            // Embed the children of this object
286            if (ldpPreferences.prefersEmbed()) {
287                streams.add(getTriples(EMBED_RESOURCES));
288            }
289        }
290
291        final RdfStream rdfStream = new DefaultRdfStream(
292                asNode(resource()), streams.stream().reduce(empty(), Stream::concat));
293
294        if (httpTripleUtil != null && ldpPreferences.prefersServerManaged()) {
295            return httpTripleUtil.addHttpComponentModelsForResourceToStream(rdfStream, resource(), uriInfo,
296                    translator());
297        }
298
299        return rdfStream;
300    }
301
302    /**
303     * Get the binary content of a datastream
304     *
305     * @param rangeValue the range value
306     * @return Binary blob
307     * @throws IOException if io exception occurred
308     */
309    protected Response getBinaryContent(final String rangeValue)
310            throws IOException {
311            final FedoraBinary binary = (FedoraBinary)resource();
312
313            // we include an explicit etag, because the default behavior is to use the JCR node's etag, not
314            // the jcr:content node digest. The etag is only included if we are not within a transaction.
315            final String txId = TransactionServiceImpl.getCurrentTransactionId(session());
316            if (txId == null) {
317                checkCacheControlHeaders(request, servletResponse, binary, session());
318            }
319            final CacheControl cc = new CacheControl();
320            cc.setMaxAge(0);
321            cc.setMustRevalidate(true);
322            Response.ResponseBuilder builder;
323
324            if (rangeValue != null && rangeValue.startsWith("bytes")) {
325
326                final Range range = Range.convert(rangeValue);
327
328                final long contentSize = binary.getContentSize();
329
330                final String endAsString;
331
332                if (range.end() == -1) {
333                    endAsString = Long.toString(contentSize - 1);
334                } else {
335                    endAsString = Long.toString(range.end());
336                }
337
338                final String contentRangeValue =
339                        String.format("bytes %s-%s/%s", range.start(),
340                                endAsString, contentSize);
341
342                if (range.end() > contentSize ||
343                        (range.end() == -1 && range.start() > contentSize)) {
344
345                    builder = status(REQUESTED_RANGE_NOT_SATISFIABLE)
346                            .header("Content-Range", contentRangeValue);
347                } else {
348                    @SuppressWarnings("resource")
349                    final RangeRequestInputStream rangeInputStream =
350                            new RangeRequestInputStream(binary.getContent(), range.start(), range.size());
351
352                    builder = status(PARTIAL_CONTENT).entity(rangeInputStream)
353                            .header("Content-Range", contentRangeValue);
354                }
355
356            } else {
357                @SuppressWarnings("resource")
358                final InputStream content = binary.getContent();
359                builder = ok(content);
360            }
361
362
363            // we set the content-type explicitly to avoid content-negotiation from getting in the way
364            return builder.type(binary.getMimeType())
365                    .cacheControl(cc)
366                    .build();
367
368        }
369
370    protected RdfStream getTriples(final Set<? extends TripleCategory> x) {
371        return getTriples(resource(), x);
372    }
373
374    protected RdfStream getTriples(final FedoraResource resource, final Set<? extends TripleCategory> x) {
375        return resource.getTriples(translator(), x);
376    }
377
378    protected RdfStream getTriples(final TripleCategory x) {
379        return getTriples(resource(), x);
380    }
381
382    protected RdfStream getTriples(final FedoraResource resource, final TripleCategory x) {
383        return resource.getTriples(translator(), x);
384    }
385
386    protected URI getUri(final FedoraResource resource) {
387        try {
388            final String uri = translator().reverse().convert(resource).getURI();
389            return new URI(uri);
390        } catch (final URISyntaxException e) {
391            throw new BadRequestException(e);
392        }
393    }
394
395    protected FedoraResource resource() {
396        if (resource == null) {
397            resource = getResourceFromPath(externalPath());
398        }
399        return resource;
400    }
401
402    protected void addResourceLinkHeaders(final FedoraResource resource) {
403        addResourceLinkHeaders(resource, false);
404    }
405
406    protected void addResourceLinkHeaders(final FedoraResource resource, final boolean includeAnchor) {
407        if (resource instanceof NonRdfSourceDescription) {
408            final URI uri = getUri(((NonRdfSourceDescription) resource).getDescribedResource());
409            final Link link = Link.fromUri(uri).rel("describes").build();
410            servletResponse.addHeader("Link", link.toString());
411        } else if (resource instanceof FedoraBinary) {
412            final URI uri = getUri(((FedoraBinary) resource).getDescription());
413            final Link.Builder builder = Link.fromUri(uri).rel("describedby");
414
415            if (includeAnchor) {
416                builder.param("anchor", getUri(resource).toString());
417            }
418            servletResponse.addHeader("Link", builder.build().toString());
419        }
420    }
421
422    /**
423     * Add any resource-specific headers to the response
424     * @param resource the resource
425     */
426    protected void addResourceHttpHeaders(final FedoraResource resource) {
427        if (resource instanceof FedoraBinary) {
428
429            final FedoraBinary binary = (FedoraBinary)resource;
430            final ContentDisposition contentDisposition = ContentDisposition.type("attachment")
431                    .fileName(binary.getFilename())
432                    .creationDate(binary.getCreatedDate())
433                    .modificationDate(binary.getLastModifiedDate())
434                    .size(binary.getContentSize())
435                    .build();
436
437            servletResponse.addHeader("Content-Type", binary.getMimeType());
438            servletResponse.addHeader("Content-Length", String.valueOf(binary.getContentSize()));
439            servletResponse.addHeader("Accept-Ranges", "bytes");
440            servletResponse.addHeader("Content-Disposition", contentDisposition.toString());
441        }
442
443        servletResponse.addHeader("Link", "<" + LDP_NAMESPACE + "Resource>;rel=\"type\"");
444
445        if (resource instanceof NonRdfSource) {
446            servletResponse.addHeader("Link", "<" + LDP_NAMESPACE + "NonRDFSource>;rel=\"type\"");
447        } else if (resource instanceof Container) {
448            servletResponse.addHeader("Link", "<" + CONTAINER.getURI() + ">;rel=\"type\"");
449            if (resource.hasType(LDP_BASIC_CONTAINER)) {
450                servletResponse.addHeader("Link", "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\"");
451            } else if (resource.hasType(LDP_DIRECT_CONTAINER)) {
452                servletResponse.addHeader("Link", "<" + DIRECT_CONTAINER.getURI() + ">;rel=\"type\"");
453            } else if (resource.hasType(LDP_INDIRECT_CONTAINER)) {
454                servletResponse.addHeader("Link", "<" + INDIRECT_CONTAINER.getURI() + ">;rel=\"type\"");
455            } else {
456                servletResponse.addHeader("Link", "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\"");
457            }
458        } else {
459            servletResponse.addHeader("Link", "<" + LDP_NAMESPACE + "RDFSource>;rel=\"type\"");
460        }
461        if (httpHeaderInject != null) {
462            httpHeaderInject.addHttpHeaderToResponseStream(servletResponse, uriInfo, resource());
463        }
464
465    }
466
467    /**
468     * Evaluate the cache control headers for the request to see if it can be served from
469     * the cache.
470     *
471     * @param request the request
472     * @param servletResponse the servlet response
473     * @param resource the fedora resource
474     * @param session the session
475     */
476    protected static void checkCacheControlHeaders(final Request request,
477                                                   final HttpServletResponse servletResponse,
478                                                   final FedoraResource resource,
479                                                   final Session session) {
480        evaluateRequestPreconditions(request, servletResponse, resource, session, true);
481        addCacheControlHeaders(servletResponse, resource, session);
482    }
483
484    /**
485     * Add ETag and Last-Modified cache control headers to the response
486     * @param servletResponse the servlet response
487     * @param resource the fedora resource
488     * @param session the session
489     */
490    protected static void addCacheControlHeaders(final HttpServletResponse servletResponse,
491                                                 final FedoraResource resource,
492                                                 final Session session) {
493
494        final String txId = TransactionServiceImpl.getCurrentTransactionId(session);
495        if (txId != null) {
496            // Do not add caching headers if in a transaction
497            return;
498        }
499
500        final FedoraResource mutableResource = resource instanceof NonRdfSourceDescription
501                ? ((NonRdfSourceDescription) resource).getDescribedResource() : resource;
502        final EntityTag etag = new EntityTag(mutableResource.getEtagValue());
503        final Date date = mutableResource.getLastModifiedDate();
504
505        if (!etag.getValue().isEmpty()) {
506            servletResponse.addHeader("ETag", etag.toString());
507        }
508
509        if (date != null) {
510            servletResponse.addDateHeader("Last-Modified", date.getTime());
511        }
512    }
513
514    /**
515     * Evaluate request preconditions to ensure the resource is the expected state
516     * @param request the request
517     * @param servletResponse the servlet response
518     * @param resource the resource
519     * @param session the session
520     */
521    protected static void evaluateRequestPreconditions(final Request request,
522                                                       final HttpServletResponse servletResponse,
523                                                       final FedoraResource resource,
524                                                       final Session session) {
525        evaluateRequestPreconditions(request, servletResponse, resource, session, false);
526    }
527
528    private static void evaluateRequestPreconditions(final Request request,
529                                                     final HttpServletResponse servletResponse,
530                                                     final FedoraResource resource,
531                                                     final Session session,
532                                                     final boolean cacheControl) {
533
534        final String txId = TransactionServiceImpl.getCurrentTransactionId(session);
535        if (txId != null) {
536            // Force cache revalidation if in a transaction
537            servletResponse.addHeader(CACHE_CONTROL, "must-revalidate");
538            servletResponse.addHeader(CACHE_CONTROL, "max-age=0");
539            return;
540        }
541
542        final EntityTag etag = new EntityTag(resource.getEtagValue());
543        final Date date = resource.getLastModifiedDate();
544        final Date roundedDate = new Date();
545
546        if (date != null) {
547            roundedDate.setTime(date.getTime() - date.getTime() % 1000);
548        }
549
550        Response.ResponseBuilder builder = request.evaluatePreconditions(etag);
551        if ( builder != null ) {
552            builder = builder.entity("ETag mismatch");
553        } else {
554            builder = request.evaluatePreconditions(roundedDate);
555            if ( builder != null ) {
556                builder = builder.entity("Date mismatch");
557            }
558        }
559
560        if (builder != null && cacheControl ) {
561            final CacheControl cc = new CacheControl();
562            cc.setMaxAge(0);
563            cc.setMustRevalidate(true);
564            // here we are implicitly emitting a 304
565            // the exception is not an error, it's genuinely
566            // an exceptional condition
567            builder = builder.cacheControl(cc).lastModified(date).tag(etag);
568        }
569        if (builder != null) {
570            throw new WebApplicationException(builder.build());
571        }
572    }
573
574    protected static MediaType getSimpleContentType(final MediaType requestContentType) {
575        return requestContentType != null ? new MediaType(requestContentType.getType(), requestContentType.getSubtype())
576                : APPLICATION_OCTET_STREAM_TYPE;
577    }
578
579    protected static boolean isRdfContentType(final String contentTypeString) {
580        return contentTypeToLang(contentTypeString) != null;
581    }
582
583    protected void replaceResourceBinaryWithStream(final FedoraBinary result,
584                                                   final InputStream requestBodyStream,
585                                                   final ContentDisposition contentDisposition,
586                                                   final MediaType contentType,
587                                                   final String checksum) throws InvalidChecksumException {
588        final URI checksumURI = checksumURI(checksum);
589        final String originalFileName = contentDisposition != null ? contentDisposition.getFileName() : "";
590        final String originalContentType = contentType != null ? contentType.toString() : "";
591
592        result.setContent(requestBodyStream,
593                originalContentType,
594                checksumURI,
595                originalFileName,
596                storagePolicyDecisionPoint);
597    }
598
599    protected void replaceResourceWithStream(final FedoraResource resource,
600                                             final InputStream requestBodyStream,
601                                             final MediaType contentType,
602                                             final RdfStream resourceTriples) throws MalformedRdfException {
603        final Lang format = contentTypeToLang(contentType.toString());
604
605        final Model inputModel = createDefaultModel();
606        try {
607            inputModel.read(requestBodyStream, getUri(resource).toString(), format.getName().toUpperCase());
608
609        } catch (final RiotException e) {
610            throw new BadRequestException("RDF was not parsable: " + e.getMessage(), e);
611
612        } catch (final RuntimeIOException e) {
613            if (e.getCause() instanceof JsonParseException) {
614                throw new MalformedRdfException(e.getCause());
615            }
616            throw new RepositoryRuntimeException(e);
617        }
618
619        resource.replaceProperties(translator(), inputModel, resourceTriples);
620    }
621
622    protected void patchResourcewithSparql(final FedoraResource resource,
623            final String requestBody,
624            final RdfStream resourceTriples) {
625        if (resource instanceof NonRdfSourceDescription) {
626            // update the described resource instead
627            ((NonRdfSourceDescription) resource).getDescribedResource()
628                    .updateProperties(translator(), requestBody, resourceTriples);
629        } else {
630            resource.updateProperties(translator(), requestBody, resourceTriples);
631        }
632    }
633
634    /**
635     * Create a checksum URI object.
636     **/
637    private static URI checksumURI( final String checksum ) {
638        if (!isBlank(checksum)) {
639            return URI.create(checksum);
640        }
641        return null;
642    }
643}