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.util.ArrayList;
040import java.util.Collections;
041import java.util.Date;
042import java.util.Iterator;
043import java.util.List;
044
045import javax.jcr.AccessDeniedException;
046import javax.jcr.ItemNotFoundException;
047import javax.jcr.Node;
048import javax.jcr.PathNotFoundException;
049import javax.jcr.Property;
050import javax.jcr.RepositoryException;
051import javax.jcr.Session;
052import javax.jcr.Value;
053import javax.jcr.version.Version;
054import javax.jcr.version.VersionHistory;
055
056import com.google.common.base.Converter;
057import com.google.common.base.Function;
058import com.google.common.base.Predicate;
059import com.google.common.collect.Iterators;
060import com.hp.hpl.jena.rdf.model.Resource;
061
062import org.fcrepo.kernel.FedoraJcrTypes;
063import org.fcrepo.kernel.models.NonRdfSourceDescription;
064import org.fcrepo.kernel.models.FedoraBinary;
065import org.fcrepo.kernel.models.FedoraResource;
066import org.fcrepo.kernel.exception.MalformedRdfException;
067import org.fcrepo.kernel.exception.PathNotFoundRuntimeException;
068import org.fcrepo.kernel.exception.RepositoryRuntimeException;
069import org.fcrepo.kernel.identifiers.IdentifierConverter;
070import org.fcrepo.kernel.impl.utils.JcrPropertyStatementListener;
071import org.fcrepo.kernel.utils.iterators.GraphDifferencingIterator;
072import org.fcrepo.kernel.impl.utils.iterators.RdfAdder;
073import org.fcrepo.kernel.impl.utils.iterators.RdfRemover;
074import org.fcrepo.kernel.utils.iterators.RdfStream;
075
076import org.modeshape.jcr.api.JcrTools;
077import org.slf4j.Logger;
078
079import com.hp.hpl.jena.rdf.model.Model;
080import com.hp.hpl.jena.update.UpdateRequest;
081
082/**
083 * Common behaviors across {@link org.fcrepo.kernel.models.Container} and
084 * {@link org.fcrepo.kernel.models.NonRdfSourceDescription} types; also used
085 * when the exact type of an object is irrelevant
086 *
087 * @author ajs6f
088 */
089public class FedoraResourceImpl extends JcrTools implements FedoraJcrTypes, FedoraResource {
090
091    private static final Logger LOGGER = getLogger(FedoraResourceImpl.class);
092
093    protected Node node;
094
095    /**
096     * Construct a {@link org.fcrepo.kernel.models.FedoraResource} from an existing JCR Node
097     * @param node an existing JCR node to treat as an fcrepo object
098     */
099    public FedoraResourceImpl(final Node node) {
100        this.node = node;
101    }
102
103    /* (non-Javadoc)
104     * @see org.fcrepo.kernel.models.FedoraResource#getNode()
105     */
106    @Override
107    public Node getNode() {
108        return node;
109    }
110
111    /* (non-Javadoc)
112     * @see org.fcrepo.kernel.models.FedoraResource#getPath()
113     */
114    @Override
115    public String getPath() {
116        try {
117            return node.getPath();
118        } catch (final RepositoryException e) {
119            throw new RepositoryRuntimeException(e);
120        }
121    }
122
123    /* (non-Javadoc)
124     * @see org.fcrepo.kernel.models.FedoraResource#getChildren()
125     */
126    @Override
127    public Iterator<FedoraResource> getChildren() {
128        try {
129            return concat(nodeToGoodChildren(node));
130        } catch (final RepositoryException e) {
131            throw new RepositoryRuntimeException(e);
132        }
133    }
134
135    /**
136     * Get the "good" children for a node by skipping all pairtree nodes in the way.
137     * @param input
138     * @return
139     * @throws RepositoryException
140     */
141    private Iterator<Iterator<FedoraResource>> nodeToGoodChildren(final Node input) throws RepositoryException {
142        final Iterator<Node> allChildren = input.getNodes();
143        final Iterator<Node> children = filter(allChildren, not(nastyChildren));
144        return transform(children, new Function<Node, Iterator<FedoraResource>>() {
145
146            @Override
147            public Iterator<FedoraResource> apply(final Node input) {
148                try {
149                    if (input.isNodeType(FEDORA_PAIRTREE)) {
150                        return concat(nodeToGoodChildren(input));
151                    }
152                    return singletonIterator(nodeToObjectBinaryConverter.convert(input));
153                } catch (final RepositoryException e) {
154                    throw new RepositoryRuntimeException(e);
155                }
156            }
157        });
158    }
159    /**
160     * Children for whom we will not generate triples.
161     */
162    private static Predicate<Node> nastyChildren =
163            new Predicate<Node>() {
164
165                @Override
166                public boolean apply(final Node n) {
167                    LOGGER.trace("Testing child node {}", n);
168                    try {
169                        return isInternalNode.apply(n)
170                                || n.getName().equals(JCR_CONTENT)
171                                || TombstoneImpl.hasMixin(n)
172                                || n.getName().equals("#");
173                    } catch (final RepositoryException e) {
174                        throw new RepositoryRuntimeException(e);
175                    }
176                }
177            };
178
179
180    private static final Converter<FedoraResource, FedoraResource> datastreamToBinary
181            = new Converter<FedoraResource, FedoraResource>() {
182
183        @Override
184        protected FedoraResource doForward(final FedoraResource fedoraResource) {
185            if (fedoraResource instanceof NonRdfSourceDescription) {
186                return ((NonRdfSourceDescription) fedoraResource).getDescribedResource();
187            }
188            return fedoraResource;
189        }
190
191        @Override
192        protected FedoraResource doBackward(final FedoraResource fedoraResource) {
193            if (fedoraResource instanceof FedoraBinary) {
194                return ((FedoraBinary) fedoraResource).getDescription();
195            }
196            return fedoraResource;
197        }
198    };
199
200    private static final Converter<Node, FedoraResource> nodeToObjectBinaryConverter
201            = nodeConverter.andThen(datastreamToBinary);
202
203    @Override
204    public FedoraResource getContainer() {
205        try {
206
207            if (getNode().getDepth() == 0) {
208                return null;
209            }
210
211            Node container = getNode().getParent();
212            while (container.getDepth() > 0) {
213                if (container.isNodeType(FEDORA_PAIRTREE)
214                        || container.isNodeType(FEDORA_NON_RDF_SOURCE_DESCRIPTION)) {
215                    container = container.getParent();
216                } else {
217                    return nodeConverter.convert(container);
218                }
219            }
220
221            return nodeConverter.convert(container);
222        } catch (final RepositoryException e) {
223            throw new RepositoryRuntimeException(e);
224        }
225    }
226
227    @Override
228    public FedoraResource getChild(final String relPath) {
229        try {
230            return nodeConverter.convert(getNode().getNode(relPath));
231        } catch (final RepositoryException e) {
232            throw new RepositoryRuntimeException(e);
233        }
234    }
235
236    @Override
237    public boolean hasProperty(final String relPath) {
238        try {
239            return getNode().hasProperty(relPath);
240        } catch (final RepositoryException e) {
241            throw new RepositoryRuntimeException(e);
242        }
243    }
244
245    @Override
246    public Property getProperty(final String relPath) {
247        try {
248            return getNode().getProperty(relPath);
249        } catch (final RepositoryException e) {
250            throw new RepositoryRuntimeException(e);
251        }
252    }
253
254    @Override
255    public void delete() {
256        try {
257            final Iterator<Property> references = node.getReferences();
258            final Iterator<Property> weakReferences = node.getWeakReferences();
259            final Iterator<Property> inboundProperties = Iterators.concat(references, weakReferences);
260
261            while (inboundProperties.hasNext()) {
262                final Property prop = inboundProperties.next();
263                final List<Value> newVals = new ArrayList<>();
264                final Iterator<Value> propIt = property2values.apply(prop);
265                while (propIt.hasNext()) {
266                    final Value v = propIt.next();
267                    if (!node.equals(getSession().getNodeByIdentifier(v.getString()))) {
268                        newVals.add(v);
269                        LOGGER.trace("Keeping multivalue reference property when deleting node");
270                    }
271                }
272                if (newVals.size() == 0) {
273                    prop.remove();
274                } else {
275                    prop.setValue(newVals.toArray(new Value[newVals.size()]));
276                }
277            }
278
279            final Node parent;
280
281            if (getNode().getDepth() > 0) {
282                parent = getNode().getParent();
283            } else {
284                parent = null;
285            }
286            final String name = getNode().getName();
287
288            node.remove();
289
290            if (parent != null) {
291                createTombstone(parent, name);
292            }
293
294        } catch (final RepositoryException e) {
295            throw new RepositoryRuntimeException(e);
296        }
297    }
298
299    private void createTombstone(final Node parent, final String path) throws RepositoryException {
300        findOrCreateChild(parent, path, FEDORA_TOMBSTONE);
301    }
302
303    /* (non-Javadoc)
304     * @see org.fcrepo.kernel.models.FedoraResource#getCreatedDate()
305     */
306    @Override
307    public Date getCreatedDate() {
308        try {
309            if (hasProperty(JCR_CREATED)) {
310                return new Date(getProperty(JCR_CREATED).getDate().getTimeInMillis());
311            }
312        } catch (final PathNotFoundException e) {
313            throw new PathNotFoundRuntimeException(e);
314        } catch (final RepositoryException e) {
315            throw new RepositoryRuntimeException(e);
316        }
317        LOGGER.debug("Node {} does not have a createdDate", node);
318        return null;
319    }
320
321    /* (non-Javadoc)
322     * @see org.fcrepo.kernel.models.FedoraResource#getLastModifiedDate()
323     */
324    @Override
325    public Date getLastModifiedDate() {
326
327        try {
328            if (hasProperty(JCR_LASTMODIFIED)) {
329                return new Date(getProperty(JCR_LASTMODIFIED).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("Could not get last modified date property for node {}", node);
337
338        final Date createdDate = getCreatedDate();
339        if (createdDate != null) {
340            LOGGER.trace("Using created date for last modified date for node {}", node);
341            return createdDate;
342        }
343
344        return null;
345    }
346
347
348    @Override
349    public boolean hasType(final String type) {
350        try {
351            if (isFrozen.apply(node) && hasProperty(FROZEN_MIXIN_TYPES)) {
352                final List<String> types = newArrayList(
353                    transform(property2values.apply(getProperty(FROZEN_MIXIN_TYPES)), value2string)
354                );
355                return types.contains(type);
356            }
357            return node.isNodeType(type);
358        } catch (final PathNotFoundException e) {
359            throw new PathNotFoundRuntimeException(e);
360        } catch (final RepositoryException e) {
361            throw new RepositoryRuntimeException(e);
362        }
363    }
364
365    /* (non-Javadoc)
366     * @see org.fcrepo.kernel.models.FedoraResource#updateProperties
367     *     (org.fcrepo.kernel.identifiers.IdentifierConverter, java.lang.String, RdfStream)
368     */
369    @Override
370    public void updateProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
371                                 final String sparqlUpdateStatement, final RdfStream originalTriples)
372            throws MalformedRdfException, AccessDeniedException {
373
374        final Model model = originalTriples.asModel();
375
376        final JcrPropertyStatementListener listener =
377                new JcrPropertyStatementListener(idTranslator, getSession());
378
379        model.register(listener);
380
381        final UpdateRequest request = create(sparqlUpdateStatement,
382                idTranslator.reverse().convert(this).toString());
383        model.setNsPrefixes(request.getPrefixMapping());
384        execute(request, model);
385
386        listener.assertNoExceptions();
387    }
388
389    @Override
390    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
391                                final Class<? extends RdfStream> context) {
392        return getTriples(idTranslator, Collections.singleton(context));
393    }
394
395    @Override
396    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
397                                final Iterable<? extends Class<? extends RdfStream>> contexts) {
398        final RdfStream stream = new RdfStream();
399
400        for (final Class<? extends RdfStream> context : contexts) {
401            try {
402                final Constructor<? extends RdfStream> declaredConstructor
403                        = context.getDeclaredConstructor(FedoraResource.class, IdentifierConverter.class);
404
405                final RdfStream rdfStream = declaredConstructor.newInstance(this, idTranslator);
406                rdfStream.session(getSession());
407
408                stream.concat(rdfStream);
409            } catch (final NoSuchMethodException |
410                    InstantiationException |
411                    IllegalAccessException e) {
412                // Shouldn't happen.
413                throw propagate(e);
414            } catch (final InvocationTargetException e) {
415                final Throwable cause = e.getCause();
416                if (cause instanceof RepositoryException) {
417                    throw new RepositoryRuntimeException(cause);
418                }
419                throw propagate(cause);
420            }
421        }
422
423        return stream;
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
590            final Node n = getFrozenNode(label);
591
592            if (n != null) {
593                return n;
594            }
595
596            if (isVersioned()) {
597                final VersionHistory hist =
598                        session.getWorkspace().getVersionManager().getVersionHistory(getPath());
599
600                if (hist.hasVersionLabel(label)) {
601                    LOGGER.debug("Found version for {} by label {}.", this, label);
602                    return hist.getVersionByLabel(label).getFrozenNode();
603                }
604            }
605
606            LOGGER.warn("Unknown version {} with label or uuid {}!", this, label);
607            return null;
608        } catch (final RepositoryException e) {
609            throw new RepositoryRuntimeException(e);
610        }
611
612    }
613
614    private Node getFrozenNode(final String label) throws RepositoryException {
615        try {
616            final Session session = getSession();
617
618            final Node frozenNode = session.getNodeByIdentifier(label);
619
620            final String baseUUID = getNode().getIdentifier();
621
622            /*
623             * We found a node whose identifier is the "label" for the version.  Now
624             * we must do due dilligence to make sure it's a frozen node representing
625             * a version of the subject node.
626             */
627            final Property p = frozenNode.getProperty("jcr:frozenUuid");
628            if (p != null) {
629                if (p.getString().equals(baseUUID)) {
630                    return frozenNode;
631                }
632            }
633            /*
634             * Though a node with an id of the label was found, it wasn't the
635             * node we were looking for, so fall through and look for a labeled
636             * node.
637             */
638        } catch (final ItemNotFoundException ex) {
639            /*
640             * the label wasn't a uuid of a frozen node but
641             * instead possibly a version label.
642             */
643        }
644        return null;
645    }
646
647    @Override
648    public boolean equals(final Object object) {
649        if (object instanceof FedoraResourceImpl) {
650            return ((FedoraResourceImpl) object).getNode().equals(this.getNode());
651        }
652        return false;
653    }
654
655    @Override
656    public int hashCode() {
657        return getNode().hashCode();
658    }
659
660    protected Session getSession() {
661        try {
662            return getNode().getSession();
663        } catch (final RepositoryException e) {
664            throw new RepositoryRuntimeException(e);
665        }
666    }
667
668    @Override
669    public String toString() {
670        return getNode().toString();
671    }
672}