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     */
071    public JcrPropertyStatementListener(final IdentifierConverter<Resource, FedoraResource> idTranslator,
072                                        final Session session, final Node topic) {
073        this(idTranslator, new JcrRdfTools(idTranslator, session), topic);
074    }
075
076    /**
077     * Construct a statement listener within the given session
078     *
079     * @param idTranslator the id translator
080     * @param jcrRdfTools the jcr rdf tools
081     */
082    public JcrPropertyStatementListener(final IdentifierConverter<Resource, FedoraResource> idTranslator,
083                                        final JcrRdfTools jcrRdfTools, final Node topic) {
084        super();
085        this.idTranslator = idTranslator;
086        this.jcrRdfTools = jcrRdfTools;
087        this.exceptions = new ArrayList<>();
088        this.topic = topic;
089    }
090
091    /**
092     * When a statement is added to the graph, serialize it to a JCR property
093     *
094     * @param input the input statement
095     */
096    @Override
097    public void addedStatement(final Statement input) {
098
099        final Resource subject = input.getSubject();
100        try {
101            validateSubject(subject);
102            LOGGER.debug(">> adding statement {}", input);
103
104            final Statement s = jcrRdfTools.skolemize(idTranslator, input);
105
106            final FedoraResource resource = idTranslator.convert(s.getSubject());
107
108            // special logic for handling rdf:type updates.
109            // if the object is an already-existing mixin, update
110            // the node's mixins. If it isn't, just treat it normally.
111            final Property property = s.getPredicate();
112            final RDFNode objectNode = s.getObject();
113            if (property.equals(RDF.type) && objectNode.isResource()) {
114                final Resource mixinResource = objectNode.asResource();
115                jcrRdfTools.addMixin(resource, mixinResource, input.getModel().getNsPrefixMap());
116                return;
117            }
118
119            jcrRdfTools.addProperty(resource, property, objectNode, input.getModel().getNsPrefixMap());
120        } catch (final ConstraintViolationException e) {
121            throw e;
122        } catch (final RepositoryException | RepositoryRuntimeException e) {
123            exceptions.add(e);
124        }
125
126    }
127
128    /**
129     * When a statement is removed, remove it from the JCR properties
130     *
131     * @param s the given statement
132     */
133    @Override
134    public void removedStatement(final Statement s) {
135        try {
136            // if it's not about the right kind of node, ignore it.
137            final Resource subject = s.getSubject();
138            validateSubject(subject);
139            LOGGER.trace(">> removing statement {}", s);
140
141            final FedoraResource resource = idTranslator.convert(subject);
142
143            // special logic for handling rdf:type updates.
144            // if the object is an already-existing mixin, update
145            // the node's mixins. If it isn't, just treat it normally.
146            final Property property = s.getPredicate();
147            final RDFNode objectNode = s.getObject();
148
149            if (property.equals(RDF.type) && objectNode.isResource()) {
150                final Resource mixinResource = objectNode.asResource();
151                jcrRdfTools.removeMixin(resource, mixinResource, s.getModel().getNsPrefixMap());
152                return;
153            }
154
155            jcrRdfTools.removeProperty(resource, property, objectNode, s.getModel().getNsPrefixMap());
156        } catch (final ConstraintViolationException e) {
157            throw e;
158        } catch (final RepositoryException | RepositoryRuntimeException e) {
159            exceptions.add(e);
160        }
161
162    }
163
164    /**
165     * If it's not the right kind of node, throw an appropriate unchecked exception.
166     *
167     * @param subject
168     */
169    private void validateSubject(final Resource subject) {
170        final String subjectURI = subject.getURI();
171        // blank nodes are okay
172        if (!subject.isAnon()) {
173            // hash URIs with the same base as the topic are okay
174            final int hashIndex = subjectURI.lastIndexOf("#");
175            if (!(hashIndex > 0 && topic.getURI().equals(subjectURI.substring(0, hashIndex)))) {
176                // the topic itself is okay
177                if (!topic.equals(subject.asNode())) {
178                    // it's a bad subject, but it could still be in-domain
179                    if (idTranslator.inDomain(subject)) {
180                        LOGGER.error("{} is not in the topic of this RDF, which is {}.", subject, topic);
181                        throw new IncorrectTripleSubjectException(subject +
182                                " is not in the topic of this RDF, which is " + topic);
183                    }
184                    // it's not even in the right domain!
185                    LOGGER.error("subject ({}) is not in repository domain.", subject);
186                    throw new OutOfDomainSubjectException(subject.asNode());
187                }
188            }
189        }
190    }
191
192    /**
193     * Assert that no exceptions were thrown while this listener was processing change
194     * @throws MalformedRdfException if malformed rdf exception occurred
195     * @throws javax.jcr.AccessDeniedException if access denied exception occurred
196     */
197    public void assertNoExceptions() throws MalformedRdfException, AccessDeniedException {
198        if (!exceptions.isEmpty()) {
199            final StringJoiner sb = new StringJoiner("\n");
200            for (final Exception e : exceptions) {
201                sb.add(e.getMessage());
202                if (e instanceof AccessDeniedException) {
203                    throw new AccessDeniedException(sb.toString());
204                }
205            }
206            throw new MalformedRdfException(sb.toString());
207        }
208    }
209}