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