001/*
002 * Copyright 2015 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.kernel.modeshape.utils;
017
018import static java.util.stream.Collectors.joining;
019import static org.slf4j.LoggerFactory.getLogger;
020
021import javax.jcr.RepositoryException;
022import javax.jcr.Session;
023
024import org.fcrepo.kernel.api.exception.AccessDeniedException;
025import org.fcrepo.kernel.api.exception.ConstraintViolationException;
026import org.fcrepo.kernel.api.exception.IncorrectTripleSubjectException;
027import org.fcrepo.kernel.api.exception.MalformedRdfException;
028import org.fcrepo.kernel.api.exception.OutOfDomainSubjectException;
029import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
030import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
031import org.fcrepo.kernel.api.models.FedoraResource;
032import org.fcrepo.kernel.modeshape.rdf.JcrRdfTools;
033
034import org.slf4j.Logger;
035
036import com.hp.hpl.jena.graph.Node;
037import com.hp.hpl.jena.rdf.listeners.StatementListener;
038import com.hp.hpl.jena.rdf.model.Resource;
039import com.hp.hpl.jena.rdf.model.Statement;
040import com.hp.hpl.jena.rdf.model.RDFNode;
041import com.hp.hpl.jena.rdf.model.Property;
042import com.hp.hpl.jena.vocabulary.RDF;
043
044import java.util.ArrayList;
045import java.util.List;
046
047/**
048 * Listen to Jena statement events, and when the statement is changed in the
049 * graph store, make the change within JCR as well.
050 *
051 * @author awoods
052 */
053public class JcrPropertyStatementListener extends StatementListener {
054
055    private static final Logger LOGGER = getLogger(JcrPropertyStatementListener.class);
056
057    private final JcrRdfTools jcrRdfTools;
058
059    private final IdentifierConverter<Resource, FedoraResource> idTranslator;
060
061    private final List<Exception> exceptions;
062
063    private final Node topic;
064
065    /**
066     * Construct a statement listener within the given session
067     *
068     * @param idTranslator the id translator
069     * @param session the session
070     * @param topic the topic of the RDF statement
071     */
072    public JcrPropertyStatementListener(final IdentifierConverter<Resource, FedoraResource> idTranslator,
073                                        final Session session, final Node topic) {
074        this(idTranslator, new JcrRdfTools(idTranslator, session), topic);
075    }
076
077    /**
078     * Construct a statement listener within the given session
079     *
080     * @param idTranslator the id translator
081     * @param jcrRdfTools the jcr rdf tools
082     * @param topic the topic of the RDF statement
083     */
084    public JcrPropertyStatementListener(final IdentifierConverter<Resource, FedoraResource> idTranslator,
085                                        final JcrRdfTools jcrRdfTools, final Node topic) {
086        super();
087        this.idTranslator = idTranslator;
088        this.jcrRdfTools = jcrRdfTools;
089        this.exceptions = new ArrayList<>();
090        this.topic = topic;
091    }
092
093    /**
094     * When a statement is added to the graph, serialize it to a JCR property
095     *
096     * @param input the input statement
097     */
098    @Override
099    public void addedStatement(final Statement input) {
100
101        final Resource subject = input.getSubject();
102        try {
103            validateSubject(subject);
104            LOGGER.debug(">> adding statement {}", input);
105
106            final Statement s = jcrRdfTools.skolemize(idTranslator, input);
107
108            final FedoraResource resource = idTranslator.convert(s.getSubject());
109
110            // special logic for handling rdf:type updates.
111            // if the object is an already-existing mixin, update
112            // the node's mixins. If it isn't, just treat it normally.
113            final Property property = s.getPredicate();
114            final RDFNode objectNode = s.getObject();
115            if (property.equals(RDF.type) && objectNode.isResource()) {
116                final Resource mixinResource = objectNode.asResource();
117                jcrRdfTools.addMixin(resource, mixinResource, input.getModel().getNsPrefixMap());
118                return;
119            }
120
121            jcrRdfTools.addProperty(resource, property, objectNode, input.getModel().getNsPrefixMap());
122        } catch (final ConstraintViolationException e) {
123            throw e;
124        } catch (final javax.jcr.AccessDeniedException e) {
125            throw new AccessDeniedException(e);
126        } catch (final RepositoryException | RepositoryRuntimeException e) {
127            exceptions.add(e);
128        }
129
130    }
131
132    /**
133     * When a statement is removed, remove it from the JCR properties
134     *
135     * @param s the given statement
136     */
137    @Override
138    public void removedStatement(final Statement s) {
139        try {
140            // if it's not about the right kind of node, ignore it.
141            final Resource subject = s.getSubject();
142            validateSubject(subject);
143            LOGGER.trace(">> removing statement {}", s);
144
145            final FedoraResource resource = idTranslator.convert(subject);
146
147            // special logic for handling rdf:type updates.
148            // if the object is an already-existing mixin, update
149            // the node's mixins. If it isn't, just treat it normally.
150            final Property property = s.getPredicate();
151            final RDFNode objectNode = s.getObject();
152
153            if (property.equals(RDF.type) && objectNode.isResource()) {
154                final Resource mixinResource = objectNode.asResource();
155                jcrRdfTools.removeMixin(resource, mixinResource, s.getModel().getNsPrefixMap());
156                return;
157            }
158
159            jcrRdfTools.removeProperty(resource, property, objectNode, s.getModel().getNsPrefixMap());
160        } catch (final ConstraintViolationException e) {
161            throw e;
162        } catch (final RepositoryException | RepositoryRuntimeException e) {
163            exceptions.add(e);
164        }
165
166    }
167
168    /**
169     * If it's not the right kind of node, throw an appropriate unchecked exception.
170     *
171     * @param subject
172     */
173    private void validateSubject(final Resource subject) {
174        final String subjectURI = subject.getURI();
175        // blank nodes are okay
176        if (!subject.isAnon()) {
177            // hash URIs with the same base as the topic are okay
178            final int hashIndex = subjectURI.lastIndexOf("#");
179            if (!(hashIndex > 0 && topic.getURI().equals(subjectURI.substring(0, hashIndex)))) {
180                // the topic itself is okay
181                if (!topic.equals(subject.asNode())) {
182                    // it's a bad subject, but it could still be in-domain
183                    if (idTranslator.inDomain(subject)) {
184                        LOGGER.error("{} is not in the topic of this RDF, which is {}.", subject, topic);
185                        throw new IncorrectTripleSubjectException(subject +
186                                " is not in the topic of this RDF, which is " + topic);
187                    }
188                    // it's not even in the right domain!
189                    LOGGER.error("subject ({}) is not in repository domain.", subject);
190                    throw new OutOfDomainSubjectException(subject.asNode());
191                }
192            }
193        }
194    }
195
196    /**
197     * Assert that no exceptions were thrown while this listener was processing change
198     */
199    public void assertNoExceptions() {
200        if (!exceptions.isEmpty()) {
201            throw new MalformedRdfException(exceptions.stream().map(Exception::getMessage).collect(joining("\n")));
202        }
203    }
204}