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