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