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.utils.ContentDigest;
031import org.fcrepo.kernel.api.utils.FixityResult;
032import org.fcrepo.kernel.api.utils.iterators.RdfStream;
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.api.FedoraJcrTypes.HAS_MIME_TYPE;
053import static org.fcrepo.kernel.api.FedoraJcrTypes.FILENAME;
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 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.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     * (non-Javadoc)
122     * @see org.fcrepo.kernel.api.models.FedoraBinary#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.api.models.FedoraBinary#setContent(java.io.InputStream,
138     * java.lang.String, java.net.URI, java.lang.String,
139     * org.fcrepo.kernel.api.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(HAS_MIME_TYPE, contentType);
156            }
157
158            if (originalFileName != null) {
159                contentNode.setProperty(FILENAME, 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.api.models.FedoraBinary#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.api.models.FedoraBinary#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.api.models.FedoraBinary#getMimeType()
243     */
244    @Override
245    public String getMimeType() {
246        try {
247            if (hasProperty(HAS_MIME_TYPE)) {
248                return getProperty(HAS_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.api.models.FedoraBinary#getFilename()
259     */
260    @Override
261    public String getFilename() {
262        try {
263            if (hasProperty(FILENAME)) {
264                return getProperty(FILENAME).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            LOGGER.debug("Checking resource: " + getPath());
287
288            final String algorithm = ContentDigest.getAlgorithm(digestUri);
289
290            final long contentSize = size < 0 ? getBinaryContent().getSize() : size;
291
292            final Collection<FixityResult> fixityResults
293                    = CacheEntryFactory.forProperty(getProperty(JCR_DATA)).checkFixity(algorithm);
294
295            return new FixityRdfContext(this, idTranslator, fixityResults, digestUri, contentSize);
296        } catch (final RepositoryException e) {
297            throw new RepositoryRuntimeException(e);
298        }
299    }
300
301    /**
302     * When deleting the binary, we also need to clean up the description document.
303     */
304    @Override
305    public void delete() {
306        final NonRdfSourceDescription description = getDescription();
307
308        super.delete();
309
310        description.delete();
311    }
312
313    @Override
314    public Version getBaseVersion() {
315        return getDescription().getBaseVersion();
316    }
317
318    private static void decorateContentNode(final Node contentNode) throws RepositoryException {
319        if (contentNode == null) {
320            LOGGER.warn("{} node appears to be null!", JCR_CONTENT);
321            return;
322        }
323        if (contentNode.canAddMixin(FEDORA_BINARY)) {
324            contentNode.addMixin(FEDORA_BINARY);
325        }
326
327        if (contentNode.hasProperty(JCR_DATA)) {
328            final Property dataProperty = contentNode.getProperty(JCR_DATA);
329            final Binary binary = (Binary) dataProperty.getBinary();
330            final String dsChecksum = binary.getHexHash();
331
332            contentSizeHistogram.update(dataProperty.getLength());
333
334            contentNode.setProperty(CONTENT_SIZE, dataProperty.getLength());
335            contentNode.setProperty(CONTENT_DIGEST, ContentDigest.asURI("SHA-1", dsChecksum).toString());
336
337            LOGGER.debug("Decorated data property at path: {}", dataProperty.getPath());
338        }
339    }
340
341    /*
342     * (non-Javadoc)
343     * @see org.fcrepo.kernel.api.models.FedoraResource#getVersionHistory()
344     */
345    @Override
346    public VersionHistory getVersionHistory() {
347        try {
348            return getSession().getWorkspace().getVersionManager().getVersionHistory(getDescription().getPath());
349        } catch (final RepositoryException e) {
350            throw new RepositoryRuntimeException(e);
351        }
352    }
353
354
355    @Override
356    public boolean isVersioned() {
357        return getDescription().isVersioned();
358    }
359
360    @Override
361    public void enableVersioning() {
362        super.enableVersioning();
363        getDescription().enableVersioning();
364    }
365
366    @Override
367    public void disableVersioning() {
368        super.disableVersioning();
369        getDescription().disableVersioning();
370    }
371
372    /**
373     * Check if the given node is a Fedora binary
374     * @param node the given node
375     * @return whether the given node is a Fedora binary
376     */
377    public static boolean hasMixin(final Node node) {
378        return isFedoraBinary.test(node);
379    }
380}