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 static com.google.common.base.Predicates.not;
019import static com.google.common.base.Throwables.propagate;
020import static com.google.common.collect.Iterators.concat;
021import static com.google.common.collect.Iterators.filter;
022import static com.google.common.collect.Iterators.singletonIterator;
023import static com.google.common.collect.Iterators.transform;
024import static com.google.common.collect.Lists.newArrayList;
025import static com.hp.hpl.jena.update.UpdateAction.execute;
026import static com.hp.hpl.jena.update.UpdateFactory.create;
027import static org.apache.commons.codec.digest.DigestUtils.shaHex;
028import static org.fcrepo.kernel.impl.identifiers.NodeResourceConverter.nodeConverter;
029import static org.fcrepo.kernel.impl.utils.FedoraTypesUtils.isFrozenNode;
030import static org.fcrepo.kernel.impl.utils.FedoraTypesUtils.isInternalNode;
031import static org.fcrepo.kernel.services.functions.JcrPropertyFunctions.isFrozen;
032import static org.fcrepo.kernel.services.functions.JcrPropertyFunctions.property2values;
033import static org.fcrepo.kernel.services.functions.JcrPropertyFunctions.value2string;
034import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
035import static org.slf4j.LoggerFactory.getLogger;
036
037import java.lang.reflect.Constructor;
038import java.lang.reflect.InvocationTargetException;
039import java.util.Collections;
040import java.util.Date;
041import java.util.Iterator;
042import java.util.List;
043
044
045import javax.jcr.ItemNotFoundException;
046import javax.jcr.Node;
047import javax.jcr.PathNotFoundException;
048import javax.jcr.Property;
049import javax.jcr.RepositoryException;
050import javax.jcr.Session;
051import javax.jcr.version.LabelExistsVersionException;
052import javax.jcr.version.Version;
053import javax.jcr.version.VersionHistory;
054
055import com.google.common.base.Converter;
056import com.google.common.base.Function;
057import com.google.common.base.Predicate;
058import com.google.common.collect.Iterators;
059import com.hp.hpl.jena.rdf.model.Resource;
060import org.fcrepo.kernel.FedoraJcrTypes;
061import org.fcrepo.kernel.models.NonRdfSourceDescription;
062import org.fcrepo.kernel.models.FedoraBinary;
063import org.fcrepo.kernel.models.FedoraResource;
064import org.fcrepo.kernel.exception.MalformedRdfException;
065import org.fcrepo.kernel.exception.PathNotFoundRuntimeException;
066import org.fcrepo.kernel.exception.RepositoryRuntimeException;
067import org.fcrepo.kernel.identifiers.IdentifierConverter;
068import org.fcrepo.kernel.impl.utils.JcrPropertyStatementListener;
069import org.fcrepo.kernel.utils.iterators.GraphDifferencingIterator;
070import org.fcrepo.kernel.impl.utils.iterators.RdfAdder;
071import org.fcrepo.kernel.impl.utils.iterators.RdfRemover;
072import org.fcrepo.kernel.utils.iterators.NodeIterator;
073import org.fcrepo.kernel.utils.iterators.PropertyIterator;
074import org.fcrepo.kernel.utils.iterators.RdfStream;
075import org.modeshape.jcr.api.JcrTools;
076import org.slf4j.Logger;
077
078import com.hp.hpl.jena.rdf.model.Model;
079import com.hp.hpl.jena.update.UpdateRequest;
080
081/**
082 * Common behaviors across FedoraObject and Datastream types; also used
083 * when the exact type of an object is irrelevant
084 *
085 * @author ajs6f
086 */
087public class FedoraResourceImpl extends JcrTools implements FedoraJcrTypes, FedoraResource {
088
089    private static final Logger LOGGER = getLogger(FedoraResourceImpl.class);
090
091    protected Node node;
092
093    /**
094     * Construct a FedoraObject from an existing JCR Node
095     * @param node an existing JCR node to treat as an fcrepo object
096     */
097    public FedoraResourceImpl(final Node node) {
098        this.node = node;
099    }
100
101    /* (non-Javadoc)
102     * @see org.fcrepo.kernel.models.FedoraResource#getNode()
103     */
104    @Override
105    public Node getNode() {
106        return node;
107    }
108
109    /* (non-Javadoc)
110     * @see org.fcrepo.kernel.models.FedoraResource#getPath()
111     */
112    @Override
113    public String getPath() {
114        try {
115            return node.getPath();
116        } catch (final RepositoryException e) {
117            throw new RepositoryRuntimeException(e);
118        }
119    }
120
121    /* (non-Javadoc)
122     * @see org.fcrepo.kernel.models.FedoraResource#getChildren()
123     */
124    @Override
125    public Iterator<FedoraResource> getChildren() {
126        try {
127            return concat(nodeToGoodChildren(node));
128        } catch (final RepositoryException e) {
129            throw new RepositoryRuntimeException(e);
130        }
131    }
132
133    /**
134     * Get the "good" children for a node by skipping all pairtree nodes in the way.
135     * @param input
136     * @return
137     * @throws RepositoryException
138     */
139    private Iterator<Iterator<FedoraResource>> nodeToGoodChildren(final Node input) throws RepositoryException {
140        final Iterator<Node> children = filter(new NodeIterator(input.getNodes()), not(nastyChildren));
141        return transform(children, new Function<Node, Iterator<FedoraResource>>() {
142
143            @Override
144            public Iterator<FedoraResource> apply(final Node input) {
145                try {
146                    if (input.isNodeType(FEDORA_PAIRTREE)) {
147                        return concat(nodeToGoodChildren(input));
148                    }
149                    return singletonIterator(nodeToObjectBinaryConverter.convert(input));
150                } catch (final RepositoryException e) {
151                    throw new RepositoryRuntimeException(e);
152                }
153            }
154        });
155    }
156    /**
157     * Children for whom we will not generate triples.
158     */
159    private static Predicate<Node> nastyChildren =
160            new Predicate<Node>() {
161
162                @Override
163                public boolean apply(final Node n) {
164                    LOGGER.trace("Testing child node {}", n);
165                    try {
166                        return isInternalNode.apply(n)
167                                || n.getName().equals(JCR_CONTENT)
168                                || TombstoneImpl.hasMixin(n)
169                                || n.getName().equals("#");
170                    } catch (final RepositoryException e) {
171                        throw new RepositoryRuntimeException(e);
172                    }
173                }
174            };
175
176
177    private static final Converter<FedoraResource, FedoraResource> datastreamToBinary
178            = new Converter<FedoraResource, FedoraResource>() {
179
180        @Override
181        protected FedoraResource doForward(final FedoraResource fedoraResource) {
182            if (fedoraResource instanceof NonRdfSourceDescription) {
183                return ((NonRdfSourceDescription) fedoraResource).getDescribedResource();
184            }
185            return fedoraResource;
186        }
187
188        @Override
189        protected FedoraResource doBackward(final FedoraResource fedoraResource) {
190            if (fedoraResource instanceof FedoraBinary) {
191                return ((FedoraBinary) fedoraResource).getDescription();
192            }
193            return fedoraResource;
194        }
195    };
196
197    private static final Converter<Node, FedoraResource> nodeToObjectBinaryConverter
198            = nodeConverter.andThen(datastreamToBinary);
199
200    @Override
201    public FedoraResource getContainer() {
202        try {
203
204            if (getNode().getDepth() == 0) {
205                return null;
206            }
207
208            Node container = getNode().getParent();
209            while (container.getDepth() > 0) {
210                if (container.isNodeType(FEDORA_PAIRTREE)
211                        || container.isNodeType(FEDORA_NON_RDF_SOURCE_DESCRIPTION)) {
212                    container = container.getParent();
213                } else {
214                    return nodeConverter.convert(container);
215                }
216            }
217
218            return nodeConverter.convert(container);
219        } catch (final RepositoryException e) {
220            throw new RepositoryRuntimeException(e);
221        }
222    }
223
224    @Override
225    public FedoraResource getChild(final String relPath) {
226        try {
227            return nodeConverter.convert(getNode().getNode(relPath));
228        } catch (final RepositoryException e) {
229            throw new RepositoryRuntimeException(e);
230        }
231    }
232
233    @Override
234    public boolean hasProperty(final String relPath) {
235        try {
236            return getNode().hasProperty(relPath);
237        } catch (final RepositoryException e) {
238            throw new RepositoryRuntimeException(e);
239        }
240    }
241
242    @Override
243    public Property getProperty(final String relPath) {
244        try {
245            return getNode().getProperty(relPath);
246        } catch (final RepositoryException e) {
247            throw new RepositoryRuntimeException(e);
248        }
249    }
250
251    @Override
252    public void delete() {
253        try {
254            final Iterator<Property> inboundProperties = Iterators.concat(
255                    new PropertyIterator(node.getReferences()),
256                    new PropertyIterator(node.getWeakReferences()));
257
258            while (inboundProperties.hasNext()) {
259                inboundProperties.next().remove();
260            }
261
262            final Node parent;
263
264            if (getNode().getDepth() > 0) {
265                parent = getNode().getParent();
266            } else {
267                parent = null;
268            }
269            final String name = getNode().getName();
270
271            node.remove();
272
273            if (parent != null) {
274                createTombstone(parent, name);
275            }
276
277        } catch (final RepositoryException e) {
278            throw new RepositoryRuntimeException(e);
279        }
280    }
281
282    private void createTombstone(final Node parent, final String path) throws RepositoryException {
283        findOrCreateChild(parent, path, FEDORA_TOMBSTONE);
284    }
285
286    /* (non-Javadoc)
287     * @see org.fcrepo.kernel.models.FedoraResource#getCreatedDate()
288     */
289    @Override
290    public Date getCreatedDate() {
291        try {
292            if (hasProperty(JCR_CREATED)) {
293                return new Date(getProperty(JCR_CREATED).getDate().getTimeInMillis());
294            }
295        } catch (final PathNotFoundException e) {
296            throw new PathNotFoundRuntimeException(e);
297        } catch (final RepositoryException e) {
298            throw new RepositoryRuntimeException(e);
299        }
300        LOGGER.debug("Node {} does not have a createdDate", node);
301        return null;
302    }
303
304    /* (non-Javadoc)
305     * @see org.fcrepo.kernel.models.FedoraResource#getLastModifiedDate()
306     */
307    @Override
308    public Date getLastModifiedDate() {
309
310        try {
311            if (hasProperty(JCR_LASTMODIFIED)) {
312                return new Date(getProperty(JCR_LASTMODIFIED).getDate().getTimeInMillis());
313            }
314        } catch (final PathNotFoundException e) {
315            throw new PathNotFoundRuntimeException(e);
316        } catch (final RepositoryException e) {
317            throw new RepositoryRuntimeException(e);
318        }
319        LOGGER.debug("Could not get last modified date property for node {}", node);
320
321        final Date createdDate = getCreatedDate();
322        if (createdDate != null) {
323            LOGGER.trace("Using created date for last modified date for node {}", node);
324            return createdDate;
325        }
326
327        return null;
328    }
329
330
331    @Override
332    public boolean hasType(final String type) {
333        try {
334            if (isFrozen.apply(node) && hasProperty(FROZEN_MIXIN_TYPES)) {
335                final List<String> types = newArrayList(
336                    transform(property2values.apply(getProperty(FROZEN_MIXIN_TYPES)), value2string)
337                );
338                return types.contains(type);
339            }
340            return node.isNodeType(type);
341        } catch (final PathNotFoundException e) {
342            throw new PathNotFoundRuntimeException(e);
343        } catch (final RepositoryException e) {
344            throw new RepositoryRuntimeException(e);
345        }
346    }
347
348    /* (non-Javadoc)
349     * @see org.fcrepo.kernel.models.FedoraResource#updateProperties
350     *     (org.fcrepo.kernel.identifiers.IdentifierConverter, java.lang.String, RdfStream)
351     */
352    @Override
353    public void updateProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
354                                 final String sparqlUpdateStatement, final RdfStream originalTriples)
355            throws MalformedRdfException {
356
357        final Model model = originalTriples.asModel();
358
359        final JcrPropertyStatementListener listener =
360                new JcrPropertyStatementListener(idTranslator, getSession());
361
362        model.register(listener);
363
364        final UpdateRequest request = create(sparqlUpdateStatement, idTranslator.reverse().convert(this).toString());
365        model.setNsPrefixes(request.getPrefixMapping());
366        execute(request, model);
367
368        listener.assertNoExceptions();
369    }
370
371    @Override
372    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
373                                final Class<? extends RdfStream> context) {
374        return getTriples(idTranslator, Collections.singleton(context));
375    }
376
377    @Override
378    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
379                                final Iterable<? extends Class<? extends RdfStream>> contexts) {
380        final RdfStream stream = new RdfStream();
381
382        for (final Class<? extends RdfStream> context : contexts) {
383            try {
384                final Constructor<? extends RdfStream> declaredConstructor
385                        = context.getDeclaredConstructor(FedoraResource.class, IdentifierConverter.class);
386
387                final RdfStream rdfStream = declaredConstructor.newInstance(this, idTranslator);
388
389                stream.concat(rdfStream);
390            } catch (final NoSuchMethodException |
391                    InstantiationException |
392                    IllegalAccessException e) {
393                // Shouldn't happen.
394                throw propagate(e);
395            } catch (final InvocationTargetException e) {
396                final Throwable cause = e.getCause();
397                if (cause instanceof RepositoryException) {
398                    throw new RepositoryRuntimeException(cause);
399                }
400                throw propagate(cause);
401            }
402        }
403
404        return stream;
405    }
406
407    /* (non-Javadoc)
408     * @see org.fcrepo.kernel.models.FedoraResource#addVersionLabel(java.lang.String)
409     */
410    @Override
411    public void addVersionLabel(final String label) {
412        try {
413            final VersionHistory versionHistory = getVersionHistory();
414            if (versionHistory.hasVersionLabel(label)) {
415                // ModeShape should do this, but it just throws a VersionException
416                // the bug has been reported here: https://issues.jboss.org/browse/MODE-2372
417                throw new LabelExistsVersionException("The specified label \"" + label
418                        + "\" is already assigned to another version of this resource!");
419            }
420            versionHistory.addVersionLabel(getBaseVersion().getName(), label, false);
421        } catch (final RepositoryException e) {
422            throw new RepositoryRuntimeException(e);
423        }
424    }
425
426    /*
427     * (non-Javadoc)
428     * @see org.fcrepo.kernel.models.FedoraResource#getBaseVersion()
429     */
430    @Override
431    public Version getBaseVersion() {
432        try {
433            return getSession().getWorkspace().getVersionManager().getBaseVersion(getPath());
434        } catch (final RepositoryException e) {
435            throw new RepositoryRuntimeException(e);
436        }
437    }
438
439    /*
440     * (non-Javadoc)
441     * @see org.fcrepo.kernel.models.FedoraResource#getVersionHistory()
442     */
443    @Override
444    public VersionHistory getVersionHistory() {
445        try {
446            return getSession().getWorkspace().getVersionManager().getVersionHistory(getPath());
447        } catch (final RepositoryException e) {
448            throw new RepositoryRuntimeException(e);
449        }
450    }
451
452    /* (non-Javadoc)
453     * @see org.fcrepo.kernel.models.FedoraResource#isNew()
454     */
455    @Override
456    public Boolean isNew() {
457        return node.isNew();
458    }
459
460    /* (non-Javadoc)
461     * @see org.fcrepo.kernel.models.FedoraResource#replaceProperties
462     *     (org.fcrepo.kernel.identifiers.IdentifierConverter, com.hp.hpl.jena.rdf.model.Model)
463     */
464    @Override
465    public void replaceProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
466        final Model inputModel, final RdfStream originalTriples) throws MalformedRdfException {
467
468        final RdfStream replacementStream = new RdfStream().namespaces(inputModel.getNsPrefixMap());
469
470        final GraphDifferencingIterator differencer =
471            new GraphDifferencingIterator(inputModel, originalTriples);
472
473        final StringBuilder exceptions = new StringBuilder();
474        try {
475            new RdfRemover(idTranslator, getSession(), replacementStream
476                    .withThisContext(differencer)).consume();
477        } catch (final MalformedRdfException e) {
478            exceptions.append(e.getMessage());
479            exceptions.append("\n");
480        }
481
482        try {
483            new RdfAdder(idTranslator, getSession(), replacementStream
484                    .withThisContext(differencer.notCommon())).consume();
485        } catch (final MalformedRdfException e) {
486            exceptions.append(e.getMessage());
487        }
488
489        if (exceptions.length() > 0) {
490            throw new MalformedRdfException(exceptions.toString());
491        }
492    }
493
494    /* (non-Javadoc)
495     * @see org.fcrepo.kernel.models.FedoraResource#getEtagValue()
496     */
497    @Override
498    public String getEtagValue() {
499        final Date lastModifiedDate = getLastModifiedDate();
500
501        if (lastModifiedDate != null) {
502            return shaHex(getPath() + lastModifiedDate.getTime());
503        }
504        return "";
505    }
506
507    @Override
508    public void enableVersioning() {
509        try {
510            node.addMixin("mix:versionable");
511        } catch (final RepositoryException e) {
512            throw new RepositoryRuntimeException(e);
513        }
514    }
515
516    @Override
517    public void disableVersioning() {
518        try {
519            node.removeMixin("mix:versionable");
520        } catch (final RepositoryException e) {
521            throw new RepositoryRuntimeException(e);
522        }
523
524    }
525
526    @Override
527    public boolean isVersioned() {
528        try {
529            return node.isNodeType("mix:versionable");
530        } catch (final RepositoryException e) {
531            throw new RepositoryRuntimeException(e);
532        }
533    }
534
535    @Override
536    public boolean isFrozenResource() {
537        return isFrozenNode.apply(this);
538    }
539
540    @Override
541    public FedoraResource getVersionedAncestor() {
542
543        try {
544            if (!isFrozenResource()) {
545                return null;
546            }
547
548            Node versionableFrozenNode = getNode();
549            FedoraResource unfrozenResource = getUnfrozenResource();
550
551            // traverse the frozen tree looking for a node whose unfrozen equivalent is versioned
552            while (!unfrozenResource.isVersioned()) {
553
554                if (versionableFrozenNode.getDepth() == 0) {
555                    return null;
556                }
557
558                // node in the frozen tree
559                versionableFrozenNode = versionableFrozenNode.getParent();
560
561                // unfrozen equivalent
562                unfrozenResource = new FedoraResourceImpl(versionableFrozenNode).getUnfrozenResource();
563            }
564
565            return new FedoraResourceImpl(versionableFrozenNode);
566        } catch (final RepositoryException e) {
567            throw new RepositoryRuntimeException(e);
568        }
569
570    }
571
572    @Override
573    public FedoraResource getUnfrozenResource() {
574        if (!isFrozenResource()) {
575            return this;
576        }
577
578        try {
579            return new FedoraResourceImpl(getSession().getNodeByIdentifier(getProperty("jcr:frozenUuid").getString()));
580        } catch (final RepositoryException e) {
581            throw new RepositoryRuntimeException(e);
582        }
583    }
584
585    @Override
586    public Node getNodeVersion(final String label) {
587        try {
588            final Session session = getSession();
589            try {
590
591                final Node frozenNode = session.getNodeByIdentifier(label);
592
593                final String baseUUID = getNode().getIdentifier();
594
595            /*
596             * We found a node whose identifier is the "label" for the version.  Now
597             * we must do due dilligence to make sure it's a frozen node representing
598             * a version of the subject node.
599             */
600                final Property p = frozenNode.getProperty("jcr:frozenUuid");
601                if (p != null) {
602                    if (p.getString().equals(baseUUID)) {
603                        return frozenNode;
604                    }
605                }
606            /*
607             * Though a node with an id of the label was found, it wasn't the
608             * node we were looking for, so fall through and look for a labeled
609             * node.
610             */
611            } catch (final ItemNotFoundException ex) {
612            /*
613             * the label wasn't a uuid of a frozen node but
614             * instead possibly a version label.
615             */
616            }
617
618            if (isVersioned()) {
619                final VersionHistory hist =
620                        session.getWorkspace().getVersionManager().getVersionHistory(getPath());
621
622                if (hist.hasVersionLabel(label)) {
623                    LOGGER.debug("Found version for {} by label {}.", this, label);
624                    return hist.getVersionByLabel(label).getFrozenNode();
625                }
626            }
627
628            LOGGER.warn("Unknown version {} with label or uuid {}!", this, label);
629            return null;
630        } catch (final RepositoryException e) {
631            throw new RepositoryRuntimeException(e);
632        }
633
634    }
635
636    @Override
637    public boolean equals(final Object object) {
638        if (object instanceof FedoraResourceImpl) {
639            return ((FedoraResourceImpl) object).getNode().equals(this.getNode());
640        }
641        return false;
642    }
643
644    @Override
645    public int hashCode() {
646        return getNode().hashCode();
647    }
648
649    protected Session getSession() {
650        try {
651            return getNode().getSession();
652        } catch (final RepositoryException e) {
653            throw new RepositoryRuntimeException(e);
654        }
655    }
656
657    @Override
658    public String toString() {
659        return getNode().toString();
660    }
661}