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 com.codahale.metrics.Counter;
021import com.codahale.metrics.Histogram;
022import com.codahale.metrics.Timer;
023import com.hp.hpl.jena.rdf.model.Resource;
024import org.fcrepo.kernel.api.exception.InvalidChecksumException;
025import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException;
026import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
027import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
028import org.fcrepo.kernel.api.models.NonRdfSourceDescription;
029import org.fcrepo.kernel.api.models.FedoraBinary;
030import org.fcrepo.kernel.api.models.FedoraResource;
031import org.fcrepo.kernel.api.services.policy.StoragePolicyDecisionPoint;
032import org.fcrepo.kernel.api.RdfStream;
033import org.fcrepo.kernel.api.utils.ContentDigest;
034import org.fcrepo.kernel.api.utils.FixityResult;
035import org.fcrepo.kernel.modeshape.rdf.impl.FixityRdfContext;
036import org.fcrepo.kernel.modeshape.utils.impl.CacheEntryFactory;
037import org.fcrepo.metrics.RegistryService;
038import org.modeshape.jcr.api.Binary;
039import org.modeshape.jcr.api.ValueFactory;
040import org.slf4j.Logger;
041
042import javax.jcr.Node;
043import javax.jcr.PathNotFoundException;
044import javax.jcr.Property;
045import javax.jcr.RepositoryException;
046import javax.jcr.version.Version;
047import javax.jcr.version.VersionHistory;
048import java.io.InputStream;
049import java.net.URI;
050import java.net.URISyntaxException;
051import java.util.Collection;
052
053import static com.codahale.metrics.MetricRegistry.name;
054import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isFedoraBinary;
055import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
056import static org.modeshape.jcr.api.JcrConstants.JCR_DATA;
057import static org.slf4j.LoggerFactory.getLogger;
058
059/**
060 * @author cabeer
061 * @since 9/19/14
062 */
063public class FedoraBinaryImpl extends FedoraResourceImpl implements FedoraBinary {
064
065    private static final Logger LOGGER = getLogger(FedoraBinaryImpl.class);
066
067
068    static final RegistryService registryService = RegistryService.getInstance();
069    static final Counter fixityCheckCounter
070            = registryService.getMetrics().counter(name(FedoraBinary.class, "fixity-check-counter"));
071
072    static final Timer timer = registryService.getMetrics().timer(
073            name(NonRdfSourceDescription.class, "fixity-check-time"));
074
075    static final Histogram contentSizeHistogram =
076            registryService.getMetrics().histogram(name(FedoraBinary.class, "content-size"));
077
078    /**
079     * Wrap an existing Node as a Fedora Binary
080     * @param node the node
081     */
082    public FedoraBinaryImpl(final Node node) {
083        super(node);
084
085        if (node.isNew()) {
086            initializeNewBinaryProperties();
087        }
088    }
089
090    private void initializeNewBinaryProperties() {
091        try {
092            decorateContentNode(node);
093        } catch (final RepositoryException e) {
094            LOGGER.warn("Count not decorate {} with FedoraBinary properties: {}", node, e);
095        }
096    }
097
098    @Override
099    public FedoraResource getDescription() {
100        try {
101            return new NonRdfSourceDescriptionImpl(getNode().getParent());
102        } catch (final RepositoryException e) {
103            throw new RepositoryRuntimeException(e);
104        }
105    }
106
107    /*
108         * (non-Javadoc)
109         * @see org.fcrepo.kernel.api.models.FedoraBinary#getContent()
110         */
111    @Override
112    public InputStream getContent() {
113        try {
114            return getBinaryContent().getStream();
115        } catch (final RepositoryException e) {
116            throw new RepositoryRuntimeException(e);
117        }
118    }
119
120    /**
121     * Retrieve the JCR Binary object
122     * @return a JCR-wrapped Binary object
123     */
124    private javax.jcr.Binary getBinaryContent() {
125        try {
126            return getProperty(JCR_DATA).getBinary();
127        } catch (final PathNotFoundException e) {
128            throw new PathNotFoundRuntimeException(e);
129        } catch (final RepositoryException e) {
130            throw new RepositoryRuntimeException(e);
131        }
132    }
133
134    /*
135     * (non-Javadoc)
136     * @see org.fcrepo.kernel.api.models.FedoraBinary#setContent(java.io.InputStream,
137     * java.lang.String, java.net.URI, java.lang.String,
138     * org.fcrepo.kernel.api.services.policy.StoragePolicyDecisionPoint)
139     */
140    @Override
141    public void setContent(final InputStream content, final String contentType,
142                           final URI checksum, final String originalFileName,
143                           final StoragePolicyDecisionPoint storagePolicyDecisionPoint)
144            throws InvalidChecksumException {
145
146        try {
147            final Node contentNode = getNode();
148
149            if (contentNode.canAddMixin(FEDORA_BINARY)) {
150                contentNode.addMixin(FEDORA_BINARY);
151            }
152
153            if (contentType != null) {
154                contentNode.setProperty(HAS_MIME_TYPE, contentType);
155            }
156
157            if (originalFileName != null) {
158                contentNode.setProperty(FILENAME, originalFileName);
159            }
160
161            LOGGER.debug("Created content node at path: {}", contentNode.getPath());
162
163            String hint = null;
164
165            if (storagePolicyDecisionPoint != null) {
166                hint = storagePolicyDecisionPoint.evaluatePolicies(this);
167            }
168            final ValueFactory modevf =
169                    (ValueFactory) node.getSession().getValueFactory();
170            final Binary binary = modevf.createBinary(content, hint);
171
172        /*
173         * This next line of code deserves explanation. If we chose for the
174         * simpler line: Property dataProperty =
175         * contentNode.setProperty(JCR_DATA, requestBodyStream); then the JCR
176         * would not block on the stream's completion, and we would return to
177         * the requester before the mutation to the repo had actually completed.
178         * So instead we use createBinary(requestBodyStream), because its
179         * contract specifies: "The passed InputStream is closed before this
180         * method returns either normally or because of an exception." which
181         * lets us block and not return until the job is done! The simpler code
182         * may still be useful to us for an asynchronous method that we develop
183         * later.
184         */
185            final Property dataProperty = contentNode.setProperty(JCR_DATA, binary);
186
187            final String dsChecksum = binary.getHexHash();
188            final URI uriChecksumString = ContentDigest.asURI("SHA-1", dsChecksum);
189            if (checksum != null &&
190                    !checksum.equals(uriChecksumString)) {
191                LOGGER.debug("Failed checksum test");
192                throw new InvalidChecksumException("Checksum Mismatch of " +
193                        uriChecksumString + " and " + checksum);
194            }
195
196            decorateContentNode(contentNode);
197            touch();
198            ((FedoraResourceImpl) getDescription()).touch();
199
200            LOGGER.debug("Created data property at path: {}", dataProperty.getPath());
201
202        } catch (final RepositoryException e) {
203            throw new RepositoryRuntimeException(e);
204        }
205    }
206
207    /*
208     * (non-Javadoc)
209     * @see org.fcrepo.kernel.api.models.FedoraBinary#getContentSize()
210     */
211    @Override
212    public long getContentSize() {
213        try {
214            if (hasProperty(CONTENT_SIZE)) {
215                return getProperty(CONTENT_SIZE).getLong();
216            }
217        } catch (final RepositoryException e) {
218            LOGGER.info("Could not get contentSize(): {}", e.getMessage());
219        }
220
221        return -1L;
222    }
223
224    /*
225     * (non-Javadoc)
226     * @see org.fcrepo.kernel.api.models.FedoraBinary#getContentDigest()
227     */
228    @Override
229    public URI getContentDigest() {
230        try {
231            if (hasProperty(CONTENT_DIGEST)) {
232                return new URI(getProperty(CONTENT_DIGEST).getString());
233            }
234        } catch (final RepositoryException | URISyntaxException e) {
235            LOGGER.info("Could not get content digest: {}", e.getMessage());
236        }
237
238        return ContentDigest.missingChecksum();
239    }
240
241    /*
242     * (non-Javadoc)
243     * @see org.fcrepo.kernel.api.models.FedoraBinary#getMimeType()
244     */
245    @Override
246    public String getMimeType() {
247        try {
248            if (hasProperty(HAS_MIME_TYPE)) {
249                return getProperty(HAS_MIME_TYPE).getString();
250            }
251            return "application/octet-stream";
252        } catch (final RepositoryException e) {
253            throw new RepositoryRuntimeException(e);
254        }
255    }
256
257    /*
258     * (non-Javadoc)
259     * @see org.fcrepo.kernel.api.models.FedoraBinary#getFilename()
260     */
261    @Override
262    public String getFilename() {
263        try {
264            if (hasProperty(FILENAME)) {
265                return getProperty(FILENAME).getString();
266            }
267            return node.getParent().getName();
268        } catch (final RepositoryException e) {
269            throw new RepositoryRuntimeException(e);
270        }
271    }
272
273    @Override
274    public RdfStream getFixity(final IdentifierConverter<Resource, FedoraResource> idTranslator) {
275        return getFixity(idTranslator, getContentDigest(), getContentSize());
276    }
277
278    @Override
279    public RdfStream getFixity(final IdentifierConverter<Resource, FedoraResource> idTranslator,
280                               final URI digestUri,
281                               final long size) {
282
283        fixityCheckCounter.inc();
284
285        try (final Timer.Context context = timer.time()) {
286
287            LOGGER.debug("Checking resource: " + getPath());
288
289            final String algorithm = ContentDigest.getAlgorithm(digestUri);
290
291            final long contentSize = size < 0 ? getBinaryContent().getSize() : size;
292
293            final Collection<FixityResult> fixityResults
294                    = CacheEntryFactory.forProperty(getProperty(JCR_DATA)).checkFixity(algorithm);
295
296            return new FixityRdfContext(this, idTranslator, fixityResults, digestUri, contentSize);
297        } catch (final RepositoryException e) {
298            throw new RepositoryRuntimeException(e);
299        }
300    }
301
302    /**
303     * When deleting the binary, we also need to clean up the description document.
304     */
305    @Override
306    public void delete() {
307        final FedoraResource description = getDescription();
308
309        super.delete();
310
311        description.delete();
312    }
313
314    @Override
315    public Version getBaseVersion() {
316        return getDescription().getBaseVersion();
317    }
318
319    private static void decorateContentNode(final Node contentNode) throws RepositoryException {
320        if (contentNode == null) {
321            LOGGER.warn("{} node appears to be null!", JCR_CONTENT);
322            return;
323        }
324        if (contentNode.canAddMixin(FEDORA_BINARY)) {
325            contentNode.addMixin(FEDORA_BINARY);
326        }
327
328        if (contentNode.hasProperty(JCR_DATA)) {
329            final Property dataProperty = contentNode.getProperty(JCR_DATA);
330            final Binary binary = (Binary) dataProperty.getBinary();
331            final String dsChecksum = binary.getHexHash();
332
333            contentSizeHistogram.update(dataProperty.getLength());
334
335            contentNode.setProperty(CONTENT_SIZE, dataProperty.getLength());
336            contentNode.setProperty(CONTENT_DIGEST, ContentDigest.asURI("SHA-1", dsChecksum).toString());
337
338            LOGGER.debug("Decorated data property at path: {}", dataProperty.getPath());
339        }
340    }
341
342    /*
343     * (non-Javadoc)
344     * @see org.fcrepo.kernel.api.models.FedoraResource#getVersionHistory()
345     */
346    @Override
347    public VersionHistory getVersionHistory() {
348        try {
349            return getVersionManager().getVersionHistory(getDescription().getPath());
350        } catch (final RepositoryException e) {
351            throw new RepositoryRuntimeException(e);
352        }
353    }
354
355
356    @Override
357    public boolean isVersioned() {
358        return getDescription().isVersioned();
359    }
360
361    @Override
362    public void enableVersioning() {
363        super.enableVersioning();
364        getDescription().enableVersioning();
365    }
366
367    @Override
368    public void disableVersioning() {
369        super.disableVersioning();
370        getDescription().disableVersioning();
371    }
372
373    /**
374     * Check if the given node is a Fedora binary
375     * @param node the given node
376     * @return whether the given node is a Fedora binary
377     */
378    public static boolean hasMixin(final Node node) {
379        return isFedoraBinary.test(node);
380    }
381}