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