001/**
002 * Copyright 2014 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.VersionHistory;
046import java.io.InputStream;
047import java.net.URI;
048import java.net.URISyntaxException;
049import java.util.Collection;
050
051import static com.codahale.metrics.MetricRegistry.name;
052import static org.fcrepo.kernel.impl.utils.FedoraTypesUtils.isFedoraBinary;
053import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
054import static org.modeshape.jcr.api.JcrConstants.JCR_DATA;
055import static org.modeshape.jcr.api.JcrConstants.JCR_MIME_TYPE;
056import static org.slf4j.LoggerFactory.getLogger;
057
058/**
059 * @author cabeer
060 * @since 9/19/14
061 */
062public class FedoraBinaryImpl extends FedoraResourceImpl implements FedoraBinary {
063
064    private static final Logger LOGGER = getLogger(FedoraBinaryImpl.class);
065
066
067    static final RegistryService registryService = RegistryService.getInstance();
068    static final Counter fixityCheckCounter
069            = registryService.getMetrics().counter(name(FedoraBinary.class, "fixity-check-counter"));
070
071    static final Timer timer = registryService.getMetrics().timer(
072            name(NonRdfSourceDescription.class, "fixity-check-time"));
073
074    static final Histogram contentSizeHistogram =
075            registryService.getMetrics().histogram(name(FedoraBinary.class, "content-size"));
076
077    /**
078     * Wrap an existing Node as a Fedora Binary
079     * @param node
080     */
081    public FedoraBinaryImpl(final Node node) {
082        super(node);
083
084        if (node.isNew()) {
085            initializeNewBinaryProperties();
086        }
087    }
088
089    private void initializeNewBinaryProperties() {
090        try {
091            decorateContentNode(node);
092        } catch (RepositoryException e) {
093            LOGGER.warn("Count not decorate {} with FedoraBinary properties: {}", node, e);
094        }
095    }
096
097    @Override
098    public NonRdfSourceDescription getDescription() {
099        try {
100            return new NonRdfSourceDescriptionImpl(getNode().getParent());
101        } catch (final RepositoryException e) {
102            throw new RepositoryRuntimeException(e);
103        }
104    }
105
106    /*
107         * (non-Javadoc)
108         * @see org.fcrepo.kernel.Datastream#getContent()
109         */
110    @Override
111    public InputStream getContent() {
112        try {
113            return getBinaryContent().getStream();
114        } catch (final RepositoryException e) {
115            throw new RepositoryRuntimeException(e);
116        }
117    }
118
119    /*
120     * (non-Javadoc)
121     * @see org.fcrepo.kernel.Datastream#getBinaryContent()
122     */
123    @Override
124    public 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.Datastream#setContent(java.io.InputStream,
137     * java.lang.String, java.net.URI, java.lang.String,
138     * org.fcrepo.kernel.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(JCR_MIME_TYPE, contentType);
155            }
156
157            if (originalFileName != null) {
158                contentNode.setProperty(PREMIS_FILE_NAME, 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(node);
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
198            LOGGER.debug("Created data property at path: {}", dataProperty.getPath());
199
200        } catch (final RepositoryException e) {
201            throw new RepositoryRuntimeException(e);
202        }
203    }
204
205    /*
206     * (non-Javadoc)
207     * @see org.fcrepo.kernel.Datastream#getContentSize()
208     */
209    @Override
210    public long getContentSize() {
211        try {
212            if (hasProperty(CONTENT_SIZE)) {
213                return getProperty(CONTENT_SIZE).getLong();
214            }
215        } catch (final RepositoryException e) {
216            LOGGER.info("Could not get contentSize(): {}", e.getMessage());
217        }
218
219        return -1L;
220    }
221
222    /*
223     * (non-Javadoc)
224     * @see org.fcrepo.kernel.Datastream#getContentDigest()
225     */
226    @Override
227    public URI getContentDigest() {
228        try {
229            if (hasProperty(CONTENT_DIGEST)) {
230                return new URI(getProperty(CONTENT_DIGEST).getString());
231            }
232        } catch (final RepositoryException | URISyntaxException e) {
233            LOGGER.info("Could not get content digest: {}", e.getMessage());
234        }
235
236        return ContentDigest.missingChecksum();
237    }
238
239    /*
240     * (non-Javadoc)
241     * @see org.fcrepo.kernel.Datastream#getMimeType()
242     */
243    @Override
244    public String getMimeType() {
245        try {
246            if (hasProperty(JCR_MIME_TYPE)) {
247                return getProperty(JCR_MIME_TYPE).getString();
248            }
249            return "application/octet-stream";
250        } catch (final RepositoryException e) {
251            throw new RepositoryRuntimeException(e);
252        }
253    }
254
255    /*
256     * (non-Javadoc)
257     * @see org.fcrepo.kernel.Datastream#getFilename()
258     */
259    @Override
260    public String getFilename() {
261        try {
262            if (hasProperty(PREMIS_FILE_NAME)) {
263                return getProperty(PREMIS_FILE_NAME).getString();
264            }
265            return node.getParent().getName();
266        } catch (final RepositoryException e) {
267            throw new RepositoryRuntimeException(e);
268        }
269    }
270
271    @Override
272    public RdfStream getFixity(final IdentifierConverter<Resource, FedoraResource> idTranslator) {
273        return getFixity(idTranslator, getContentDigest(), getContentSize());
274    }
275
276    @Override
277    public RdfStream getFixity(final IdentifierConverter<Resource, FedoraResource> idTranslator,
278                               final URI digestUri,
279                               final long size) {
280
281        fixityCheckCounter.inc();
282
283        try (final Timer.Context context = timer.time()) {
284
285            final Repository repo = node.getSession().getRepository();
286            LOGGER.debug("Checking resource: " + getPath());
287
288            final String algorithm = ContentDigest.getAlgorithm(digestUri);
289
290            final Collection<FixityResult> fixityResults
291                    = CacheEntryFactory.forProperty(repo, getProperty(JCR_DATA)).checkFixity(algorithm);
292
293            return new FixityRdfContext(this, idTranslator, fixityResults, digestUri, size);
294        } catch (final RepositoryException e) {
295            throw new RepositoryRuntimeException(e);
296        }
297    }
298
299    /**
300     * When deleting the binary, we also need to clean up the description document.
301     */
302    @Override
303    public void delete() {
304        final NonRdfSourceDescription description = getDescription();
305
306        super.delete();
307
308        description.delete();
309    }
310
311    @Override
312    public void addVersionLabel(final String label) {
313        getDescription().addVersionLabel(label);
314    }
315
316    private static void decorateContentNode(final Node contentNode) throws RepositoryException {
317        if (contentNode == null) {
318            LOGGER.warn("{} node appears to be null!", JCR_CONTENT);
319            return;
320        }
321        if (contentNode.canAddMixin(FEDORA_BINARY)) {
322            contentNode.addMixin(FEDORA_BINARY);
323        }
324
325        if (contentNode.hasProperty(JCR_DATA)) {
326            final Property dataProperty = contentNode.getProperty(JCR_DATA);
327            final Binary binary = (Binary) dataProperty.getBinary();
328            final String dsChecksum = binary.getHexHash();
329
330            contentSizeHistogram.update(dataProperty.getLength());
331
332            contentNode.setProperty(CONTENT_SIZE, dataProperty.getLength());
333            contentNode.setProperty(CONTENT_DIGEST, ContentDigest.asURI("SHA-1", dsChecksum).toString());
334
335            LOGGER.debug("Decorated data property at path: {}", dataProperty.getPath());
336        }
337    }
338
339    /*
340     * (non-Javadoc)
341     * @see org.fcrepo.kernel.models.FedoraResource#getVersionHistory()
342     */
343    @Override
344    public VersionHistory getVersionHistory() {
345        try {
346            return getSession().getWorkspace().getVersionManager().getVersionHistory(getDescription().getPath());
347        } catch (final RepositoryException e) {
348            throw new RepositoryRuntimeException(e);
349        }
350    }
351
352
353    @Override
354    public boolean isVersioned() {
355        return getDescription().isVersioned();
356    }
357
358    @Override
359    public void enableVersioning() {
360        super.enableVersioning();
361        getDescription().enableVersioning();
362    }
363
364    @Override
365    public void disableVersioning() {
366        super.disableVersioning();
367        getDescription().disableVersioning();
368    }
369
370    /**
371     * Check if the given node is a Fedora binary
372     * @param node
373     * @return
374     */
375    public static boolean hasMixin(final Node node) {
376        return isFedoraBinary.apply(node);
377    }
378}