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.Collections;
040import java.util.Date;
041import java.util.Iterator;
042import java.util.List;
043
044import javax.jcr.ItemNotFoundException;
045import javax.jcr.Node;
046import javax.jcr.PathNotFoundException;
047import javax.jcr.Property;
048import javax.jcr.RepositoryException;
049import javax.jcr.Session;
050import javax.jcr.version.Version;
051import javax.jcr.version.VersionHistory;
052
053import com.google.common.base.Converter;
054import com.google.common.base.Function;
055import com.google.common.base.Predicate;
056import com.google.common.collect.Iterators;
057import com.hp.hpl.jena.rdf.model.Resource;
058
059import org.fcrepo.kernel.FedoraJcrTypes;
060import org.fcrepo.kernel.models.NonRdfSourceDescription;
061import org.fcrepo.kernel.models.FedoraBinary;
062import org.fcrepo.kernel.models.FedoraResource;
063import org.fcrepo.kernel.exception.MalformedRdfException;
064import org.fcrepo.kernel.exception.PathNotFoundRuntimeException;
065import org.fcrepo.kernel.exception.RepositoryRuntimeException;
066import org.fcrepo.kernel.identifiers.IdentifierConverter;
067import org.fcrepo.kernel.impl.utils.JcrPropertyStatementListener;
068import org.fcrepo.kernel.utils.iterators.GraphDifferencingIterator;
069import org.fcrepo.kernel.impl.utils.iterators.RdfAdder;
070import org.fcrepo.kernel.impl.utils.iterators.RdfRemover;
071import org.fcrepo.kernel.utils.iterators.RdfStream;
072
073import org.modeshape.jcr.api.JcrTools;
074import org.slf4j.Logger;
075
076import com.hp.hpl.jena.rdf.model.Model;
077import com.hp.hpl.jena.update.UpdateRequest;
078
079/**
080 * Common behaviors across {@link org.fcrepo.kernel.models.Container} and
081 * {@link org.fcrepo.kernel.models.NonRdfSourceDescription} types; also used
082 * when the exact type of an object is irrelevant
083 *
084 * @author ajs6f
085 */
086public class FedoraResourceImpl extends JcrTools implements FedoraJcrTypes, FedoraResource {
087
088    private static final Logger LOGGER = getLogger(FedoraResourceImpl.class);
089
090    protected Node node;
091
092    /**
093     * Construct a {@link org.fcrepo.kernel.models.FedoraResource} from an existing JCR Node
094     * @param node an existing JCR node to treat as an fcrepo object
095     */
096    public FedoraResourceImpl(final Node node) {
097        this.node = node;
098    }
099
100    /* (non-Javadoc)
101     * @see org.fcrepo.kernel.models.FedoraResource#getNode()
102     */
103    @Override
104    public Node getNode() {
105        return node;
106    }
107
108    /* (non-Javadoc)
109     * @see org.fcrepo.kernel.models.FedoraResource#getPath()
110     */
111    @Override
112    public String getPath() {
113        try {
114            return node.getPath();
115        } catch (final RepositoryException e) {
116            throw new RepositoryRuntimeException(e);
117        }
118    }
119
120    /* (non-Javadoc)
121     * @see org.fcrepo.kernel.models.FedoraResource#getChildren()
122     */
123    @Override
124    public Iterator<FedoraResource> getChildren() {
125        try {
126            return concat(nodeToGoodChildren(node));
127        } catch (final RepositoryException e) {
128            throw new RepositoryRuntimeException(e);
129        }
130    }
131
132    /**
133     * Get the "good" children for a node by skipping all pairtree nodes in the way.
134     * @param input
135     * @return
136     * @throws RepositoryException
137     */
138    private Iterator<Iterator<FedoraResource>> nodeToGoodChildren(final Node input) throws RepositoryException {
139        final Iterator<Node> allChildren = input.getNodes();
140        final Iterator<Node> children = filter(allChildren, 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> references = node.getReferences();
255            final Iterator<Property> weakReferences = node.getWeakReferences();
256            final Iterator<Property> inboundProperties = Iterators.concat(references, weakReferences);
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    /*
408     * (non-Javadoc)
409     * @see org.fcrepo.kernel.models.FedoraResource#getBaseVersion()
410     */
411    @Override
412    public Version getBaseVersion() {
413        try {
414            return getSession().getWorkspace().getVersionManager().getBaseVersion(getPath());
415        } catch (final RepositoryException e) {
416            throw new RepositoryRuntimeException(e);
417        }
418    }
419
420    /*
421     * (non-Javadoc)
422     * @see org.fcrepo.kernel.models.FedoraResource#getVersionHistory()
423     */
424    @Override
425    public VersionHistory getVersionHistory() {
426        try {
427            return getSession().getWorkspace().getVersionManager().getVersionHistory(getPath());
428        } catch (final RepositoryException e) {
429            throw new RepositoryRuntimeException(e);
430        }
431    }
432
433    /* (non-Javadoc)
434     * @see org.fcrepo.kernel.models.FedoraResource#isNew()
435     */
436    @Override
437    public Boolean isNew() {
438        return node.isNew();
439    }
440
441    /* (non-Javadoc)
442     * @see org.fcrepo.kernel.models.FedoraResource#replaceProperties
443     *     (org.fcrepo.kernel.identifiers.IdentifierConverter, com.hp.hpl.jena.rdf.model.Model)
444     */
445    @Override
446    public void replaceProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
447        final Model inputModel, final RdfStream originalTriples) throws MalformedRdfException {
448
449        final RdfStream replacementStream = new RdfStream().namespaces(inputModel.getNsPrefixMap());
450
451        final GraphDifferencingIterator differencer =
452            new GraphDifferencingIterator(inputModel, originalTriples);
453
454        final StringBuilder exceptions = new StringBuilder();
455        try {
456            new RdfRemover(idTranslator, getSession(), replacementStream
457                    .withThisContext(differencer)).consume();
458        } catch (final MalformedRdfException e) {
459            exceptions.append(e.getMessage());
460            exceptions.append("\n");
461        }
462
463        try {
464            new RdfAdder(idTranslator, getSession(), replacementStream
465                    .withThisContext(differencer.notCommon())).consume();
466        } catch (final MalformedRdfException e) {
467            exceptions.append(e.getMessage());
468        }
469
470        if (exceptions.length() > 0) {
471            throw new MalformedRdfException(exceptions.toString());
472        }
473    }
474
475    /* (non-Javadoc)
476     * @see org.fcrepo.kernel.models.FedoraResource#getEtagValue()
477     */
478    @Override
479    public String getEtagValue() {
480        final Date lastModifiedDate = getLastModifiedDate();
481
482        if (lastModifiedDate != null) {
483            return shaHex(getPath() + lastModifiedDate.getTime());
484        }
485        return "";
486    }
487
488    @Override
489    public void enableVersioning() {
490        try {
491            node.addMixin("mix:versionable");
492        } catch (final RepositoryException e) {
493            throw new RepositoryRuntimeException(e);
494        }
495    }
496
497    @Override
498    public void disableVersioning() {
499        try {
500            node.removeMixin("mix:versionable");
501        } catch (final RepositoryException e) {
502            throw new RepositoryRuntimeException(e);
503        }
504
505    }
506
507    @Override
508    public boolean isVersioned() {
509        try {
510            return node.isNodeType("mix:versionable");
511        } catch (final RepositoryException e) {
512            throw new RepositoryRuntimeException(e);
513        }
514    }
515
516    @Override
517    public boolean isFrozenResource() {
518        return isFrozenNode.apply(this);
519    }
520
521    @Override
522    public FedoraResource getVersionedAncestor() {
523
524        try {
525            if (!isFrozenResource()) {
526                return null;
527            }
528
529            Node versionableFrozenNode = getNode();
530            FedoraResource unfrozenResource = getUnfrozenResource();
531
532            // traverse the frozen tree looking for a node whose unfrozen equivalent is versioned
533            while (!unfrozenResource.isVersioned()) {
534
535                if (versionableFrozenNode.getDepth() == 0) {
536                    return null;
537                }
538
539                // node in the frozen tree
540                versionableFrozenNode = versionableFrozenNode.getParent();
541
542                // unfrozen equivalent
543                unfrozenResource = new FedoraResourceImpl(versionableFrozenNode).getUnfrozenResource();
544            }
545
546            return new FedoraResourceImpl(versionableFrozenNode);
547        } catch (final RepositoryException e) {
548            throw new RepositoryRuntimeException(e);
549        }
550
551    }
552
553    @Override
554    public FedoraResource getUnfrozenResource() {
555        if (!isFrozenResource()) {
556            return this;
557        }
558
559        try {
560            return new FedoraResourceImpl(getSession().getNodeByIdentifier(getProperty("jcr:frozenUuid").getString()));
561        } catch (final RepositoryException e) {
562            throw new RepositoryRuntimeException(e);
563        }
564    }
565
566    @Override
567    public Node getNodeVersion(final String label) {
568        try {
569            final Session session = getSession();
570            try {
571
572                final Node frozenNode = session.getNodeByIdentifier(label);
573
574                final String baseUUID = getNode().getIdentifier();
575
576            /*
577             * We found a node whose identifier is the "label" for the version.  Now
578             * we must do due dilligence to make sure it's a frozen node representing
579             * a version of the subject node.
580             */
581                final Property p = frozenNode.getProperty("jcr:frozenUuid");
582                if (p != null) {
583                    if (p.getString().equals(baseUUID)) {
584                        return frozenNode;
585                    }
586                }
587            /*
588             * Though a node with an id of the label was found, it wasn't the
589             * node we were looking for, so fall through and look for a labeled
590             * node.
591             */
592            } catch (final ItemNotFoundException ex) {
593            /*
594             * the label wasn't a uuid of a frozen node but
595             * instead possibly a version label.
596             */
597            }
598
599            if (isVersioned()) {
600                final VersionHistory hist =
601                        session.getWorkspace().getVersionManager().getVersionHistory(getPath());
602
603                if (hist.hasVersionLabel(label)) {
604                    LOGGER.debug("Found version for {} by label {}.", this, label);
605                    return hist.getVersionByLabel(label).getFrozenNode();
606                }
607            }
608
609            LOGGER.warn("Unknown version {} with label or uuid {}!", this, label);
610            return null;
611        } catch (final RepositoryException e) {
612            throw new RepositoryRuntimeException(e);
613        }
614
615    }
616
617    @Override
618    public boolean equals(final Object object) {
619        if (object instanceof FedoraResourceImpl) {
620            return ((FedoraResourceImpl) object).getNode().equals(this.getNode());
621        }
622        return false;
623    }
624
625    @Override
626    public int hashCode() {
627        return getNode().hashCode();
628    }
629
630    protected Session getSession() {
631        try {
632            return getNode().getSession();
633        } catch (final RepositoryException e) {
634            throw new RepositoryRuntimeException(e);
635        }
636    }
637
638    @Override
639    public String toString() {
640        return getNode().toString();
641    }
642}