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.kernel.modeshape;
019
020import static org.fcrepo.kernel.api.utils.ContentDigest.DIGEST_ALGORITHM.SHA1;
021import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
022import static org.modeshape.jcr.api.JcrConstants.JCR_DATA;
023import static org.slf4j.LoggerFactory.getLogger;
024
025import java.io.InputStream;
026import java.net.URI;
027import java.util.Collection;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.Map;
031import java.util.stream.Collectors;
032
033import javax.jcr.Node;
034import javax.jcr.PathNotFoundException;
035import javax.jcr.Property;
036import javax.jcr.RepositoryException;
037
038import org.apache.jena.rdf.model.Resource;
039import org.fcrepo.kernel.api.RdfStream;
040import org.fcrepo.kernel.api.exception.InvalidChecksumException;
041import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException;
042import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
043import org.fcrepo.kernel.api.exception.UnsupportedAlgorithmException;
044import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
045import org.fcrepo.kernel.api.models.FedoraResource;
046import org.fcrepo.kernel.api.services.policy.StoragePolicyDecisionPoint;
047import org.fcrepo.kernel.api.utils.CacheEntry;
048import org.fcrepo.kernel.api.utils.ContentDigest;
049import org.fcrepo.kernel.api.utils.FixityResult;
050import org.fcrepo.kernel.modeshape.rdf.impl.FixityRdfContext;
051import org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils;
052import org.fcrepo.kernel.modeshape.utils.impl.CacheEntryFactory;
053import org.modeshape.jcr.api.Binary;
054import org.modeshape.jcr.api.ValueFactory;
055import org.slf4j.Logger;
056
057/**
058 * Fedora Binary stored internally in modeshape
059 *
060 * @author bbpennel
061 */
062public class InternalFedoraBinary extends AbstractFedoraBinary {
063
064    private static final Logger LOGGER = getLogger(InternalFedoraBinary.class);
065
066    /**
067     * Construct InternalFedoraBinary
068     *
069     * @param node node
070     */
071    public InternalFedoraBinary(final Node node) {
072        super(node);
073    }
074
075    /*
076     * (non-Javadoc)
077     * @see org.fcrepo.kernel.api.models.FedoraBinary#getContent()
078     */
079    @Override
080    public InputStream getContent() {
081        try {
082            return getBinaryContent().getStream();
083        } catch (final RepositoryException e) {
084            throw new RepositoryRuntimeException(e);
085        }
086    }
087
088    /**
089     * Retrieve the JCR Binary object
090     *
091     * @return a JCR-wrapped Binary object
092     */
093    private javax.jcr.Binary getBinaryContent() {
094        try {
095            return getProperty(JCR_DATA).getBinary();
096        } catch (final PathNotFoundException e) {
097            throw new PathNotFoundRuntimeException(e);
098        } catch (final RepositoryException e) {
099            throw new RepositoryRuntimeException(e);
100        }
101    }
102
103    @Override
104    public void setExternalContent(final String contentType, final Collection<URI> checksums,
105                                   final String originalFileName, final String externalHandling,
106                                   final String externalUrl) {
107        throw new UnsupportedOperationException("Cannot call setExternalContent from this context");
108    }
109
110    /*
111     * (non-Javadoc)
112     * @see org.fcrepo.kernel.api.models.FedoraBinary#setContent(java.io.InputStream, java.lang.String, java.net.URI,
113     * java.lang.String, org.fcrepo.kernel.api.services.policy.StoragePolicyDecisionPoint)
114     */
115    @Override
116    public void setContent(final InputStream content, final String contentType,
117            final Collection<URI> checksums, final String originalFileName,
118            final StoragePolicyDecisionPoint storagePolicyDecisionPoint)
119            throws InvalidChecksumException {
120
121        try {
122
123
124            final Node dsNode = getNode();
125
126            LOGGER.debug("Created content node at path: {}", dsNode.getPath());
127
128            String hint = null;
129
130            if (storagePolicyDecisionPoint != null) {
131                hint = storagePolicyDecisionPoint.evaluatePolicies(this);
132            }
133            final ValueFactory modevf =
134                    (ValueFactory) node.getSession().getValueFactory();
135            final Binary binary = modevf.createBinary(content, hint);
136
137        /*
138         * This next line of code deserves explanation. If we chose for the
139         * simpler line: Property dataProperty =
140         * contentNode.setProperty(JCR_DATA, requestBodyStream); then the JCR
141         * would not block on the stream's completion, and we would return to
142         * the requester before the mutation to the repo had actually completed.
143         * So instead we use createBinary(requestBodyStream), because its
144         * contract specifies: "The passed InputStream is closed before this
145         * method returns either normally or because of an exception." which
146         * lets us block and not return until the job is done! The simpler code
147         * may still be useful to us for an asynchronous method that we develop
148         * later.
149         */
150            final Property dataProperty = dsNode.setProperty(JCR_DATA, binary);
151
152            // Ensure provided checksums are valid
153            final Collection<URI> nonNullChecksums = (null == checksums) ? new HashSet<>() : checksums;
154            verifyChecksums(nonNullChecksums, dataProperty);
155
156            final Node descNode = getDescriptionNodeOrNull();
157
158            decorateContentNode(dsNode, descNode, nonNullChecksums);
159            FedoraTypesUtils.touch(dsNode);
160
161            if (descNode != null) {
162                descNode.setProperty(HAS_MIME_TYPE, contentType);
163
164                if (originalFileName != null) {
165                    descNode.setProperty(FILENAME, originalFileName);
166                }
167
168                FedoraTypesUtils.touch(descNode);
169            }
170
171            LOGGER.debug("Created data property at path: {}", dataProperty.getPath());
172
173        } catch (final RepositoryException e) {
174            throw new RepositoryRuntimeException(e);
175        }
176    }
177
178    /*
179     * (non-Javadoc)
180     * @see org.fcrepo.kernel.api.models.FedoraBinary#getMimeType()
181     */
182    @Override
183    public String getMimeType() {
184        final String mimeType = getMimeTypeValue();
185        if (mimeType == null) {
186            return "application/octet-stream";
187        } else {
188            return mimeType;
189        }
190    }
191
192    /**
193     * This method ensures that the arg checksums are valid against the binary associated with the arg dataProperty.
194     * If one or more of the checksums are invalid, an InvalidChecksumException is thrown.
195     *
196     * @param checksums that the user provided
197     * @param dataProperty containing the binary against which the checksums will be verified
198     * @throws InvalidChecksumException on error
199     */
200    private void verifyChecksums(final Collection<URI> checksums, final Property dataProperty)
201            throws InvalidChecksumException {
202
203        final Map<URI, URI> checksumErrors = new HashMap<>();
204
205        // Loop through provided checksums validating against computed values
206        checksums.forEach(checksum -> {
207            final String algorithm = ContentDigest.getAlgorithm(checksum);
208            try {
209                // The case internally supported by ModeShape
210                if (algorithm.equals(SHA1.algorithm)) {
211                    final String dsSHA1 = ((Binary) dataProperty.getBinary()).getHexHash();
212                    final URI dsSHA1Uri = ContentDigest.asURI(SHA1.algorithm, dsSHA1);
213
214                    if (!dsSHA1Uri.equals(checksum)) {
215                        LOGGER.debug("Failed checksum test");
216                        checksumErrors.put(checksum, dsSHA1Uri);
217                    }
218
219                    // The case that requires re-computing the checksum
220                } else {
221                    final CacheEntry cacheEntry = CacheEntryFactory.forProperty(dataProperty);
222                    cacheEntry.checkFixity(algorithm).stream().findFirst().ifPresent(
223                            fixityResult -> {
224                                if (!fixityResult.matches(checksum)) {
225                                    LOGGER.debug("Failed checksum test");
226                                    checksumErrors.put(checksum, fixityResult.getComputedChecksum());
227                                }
228                            });
229                }
230            } catch (final RepositoryException e) {
231                throw new RepositoryRuntimeException(e);
232            }
233        });
234
235        // Throw an exception if any checksum errors occurred
236        if (!checksumErrors.isEmpty()) {
237            final String template = "Checksum Mismatch of %1$s and %2$s\n";
238            final StringBuilder error = new StringBuilder();
239            checksumErrors.forEach((key, value) -> error.append(String.format(template, key, value)));
240            throw new InvalidChecksumException(error.toString());
241        }
242
243    }
244
245    @Override
246    public RdfStream getFixity(final IdentifierConverter<Resource, FedoraResource> idTranslator,
247            final URI digestUri,
248            final long size) {
249
250        try {
251
252            LOGGER.debug("Checking resource: {}" + getPath());
253
254            final String algorithm = ContentDigest.getAlgorithm(digestUri);
255
256            final long contentSize = size < 0 ? getBinaryContent().getSize() : size;
257
258            final Collection<FixityResult> fixityResults = CacheEntryFactory.forProperty(getProperty(JCR_DATA))
259                    .checkFixity(algorithm);
260
261            return new FixityRdfContext(this, idTranslator, fixityResults, digestUri, contentSize);
262        } catch (final RepositoryException e) {
263            throw new RepositoryRuntimeException(e);
264        }
265    }
266
267    @Override
268    public Collection<URI> checkFixity(final IdentifierConverter<Resource, FedoraResource> idTranslator,
269            final Collection<String> algorithms)
270            throws UnsupportedAlgorithmException {
271
272        try {
273
274            LOGGER.debug("Checking resource: {}", getPath());
275            return CacheEntryFactory.forProperty(getProperty(JCR_DATA)).checkFixity(algorithms);
276        } catch (final RepositoryException e) {
277            throw new RepositoryRuntimeException(e);
278        }
279    }
280
281    /**
282     * Add necessary information to node
283     * @param dsNode The target binary node to add information to
284     * @param descNode The description node associated with the binary node
285     * @param checksums The checksum information
286     * @throws RepositoryException on error
287     */
288    private static void decorateContentNode(final Node dsNode, final Node descNode, final Collection<URI> checksums)
289        throws RepositoryException {
290
291        if (dsNode == null) {
292            LOGGER.warn("{} node appears to be null!", JCR_CONTENT);
293            return;
294        }
295
296        if (dsNode.hasProperty(JCR_DATA)) {
297            final Property dataProperty = dsNode.getProperty(JCR_DATA);
298            final Binary binary = (Binary) dataProperty.getBinary();
299            final String dsChecksum = binary.getHexHash();
300
301            checksums.add(ContentDigest.asURI(SHA1.algorithm, dsChecksum));
302
303            final String[] checksumArray = new String[checksums.size()];
304            checksums.stream().map(Object::toString).collect(Collectors.toSet()).toArray(checksumArray);
305
306            if (descNode != null) {
307                descNode.setProperty(CONTENT_DIGEST, checksumArray);
308                descNode.setProperty(CONTENT_SIZE, dataProperty.getLength());
309            }
310
311            LOGGER.debug("Decorated data property at path: {}", dataProperty.getPath());
312        }
313    }
314
315    @Override
316    public Boolean isRedirect() {
317        return false;
318    }
319
320    @Override
321    public Boolean isProxy() {
322        return false;
323    }
324
325}