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