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 org.slf4j.LoggerFactory.getLogger;
019
020import javax.jcr.AccessDeniedException;
021import javax.jcr.RepositoryException;
022import javax.jcr.Session;
023
024import org.fcrepo.kernel.api.exception.ConstraintViolationException;
025import org.fcrepo.kernel.api.exception.IncorrectTripleSubjectException;
026import org.fcrepo.kernel.api.exception.MalformedRdfException;
027import org.fcrepo.kernel.api.exception.OutOfDomainSubjectException;
028import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
029import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
030import org.fcrepo.kernel.api.models.FedoraResource;
031import org.fcrepo.kernel.modeshape.rdf.JcrRdfTools;
032
033import org.slf4j.Logger;
034
035import com.hp.hpl.jena.graph.Node;
036import com.hp.hpl.jena.rdf.listeners.StatementListener;
037import com.hp.hpl.jena.rdf.model.Resource;
038import com.hp.hpl.jena.rdf.model.Statement;
039import com.hp.hpl.jena.rdf.model.RDFNode;
040import com.hp.hpl.jena.rdf.model.Property;
041import com.hp.hpl.jena.vocabulary.RDF;
042
043import java.util.ArrayList;
044import java.util.List;
045import java.util.StringJoiner;
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 RepositoryException | RepositoryRuntimeException e) {
125            exceptions.add(e);
126        }
127
128    }
129
130    /**
131     * When a statement is removed, remove it from the JCR properties
132     *
133     * @param s the given statement
134     */
135    @Override
136    public void removedStatement(final Statement s) {
137        try {
138            // if it's not about the right kind of node, ignore it.
139            final Resource subject = s.getSubject();
140            validateSubject(subject);
141            LOGGER.trace(">> removing statement {}", s);
142
143            final FedoraResource resource = idTranslator.convert(subject);
144
145            // special logic for handling rdf:type updates.
146            // if the object is an already-existing mixin, update
147            // the node's mixins. If it isn't, just treat it normally.
148            final Property property = s.getPredicate();
149            final RDFNode objectNode = s.getObject();
150
151            if (property.equals(RDF.type) && objectNode.isResource()) {
152                final Resource mixinResource = objectNode.asResource();
153                jcrRdfTools.removeMixin(resource, mixinResource, s.getModel().getNsPrefixMap());
154                return;
155            }
156
157            jcrRdfTools.removeProperty(resource, property, objectNode, s.getModel().getNsPrefixMap());
158        } catch (final ConstraintViolationException e) {
159            throw e;
160        } catch (final RepositoryException | RepositoryRuntimeException e) {
161            exceptions.add(e);
162        }
163
164    }
165
166    /**
167     * If it's not the right kind of node, throw an appropriate unchecked exception.
168     *
169     * @param subject
170     */
171    private void validateSubject(final Resource subject) {
172        final String subjectURI = subject.getURI();
173        // blank nodes are okay
174        if (!subject.isAnon()) {
175            // hash URIs with the same base as the topic are okay
176            final int hashIndex = subjectURI.lastIndexOf("#");
177            if (!(hashIndex > 0 && topic.getURI().equals(subjectURI.substring(0, hashIndex)))) {
178                // the topic itself is okay
179                if (!topic.equals(subject.asNode())) {
180                    // it's a bad subject, but it could still be in-domain
181                    if (idTranslator.inDomain(subject)) {
182                        LOGGER.error("{} is not in the topic of this RDF, which is {}.", subject, topic);
183                        throw new IncorrectTripleSubjectException(subject +
184                                " is not in the topic of this RDF, which is " + topic);
185                    }
186                    // it's not even in the right domain!
187                    LOGGER.error("subject ({}) is not in repository domain.", subject);
188                    throw new OutOfDomainSubjectException(subject.asNode());
189                }
190            }
191        }
192    }
193
194    /**
195     * Assert that no exceptions were thrown while this listener was processing change
196     * @throws MalformedRdfException if malformed rdf exception occurred
197     * @throws javax.jcr.AccessDeniedException if access denied exception occurred
198     */
199    public void assertNoExceptions() throws MalformedRdfException, AccessDeniedException {
200        if (!exceptions.isEmpty()) {
201            final StringJoiner sb = new StringJoiner("\n");
202            for (final Exception e : exceptions) {
203                sb.add(e.getMessage());
204                if (e instanceof AccessDeniedException) {
205                    throw new AccessDeniedException(sb.toString());
206                }
207            }
208            throw new MalformedRdfException(sb.toString());
209        }
210    }
211}