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.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.models.FedoraResource;
025import org.fcrepo.kernel.exception.MalformedRdfException;
026import org.fcrepo.kernel.exception.RepositoryRuntimeException;
027import org.fcrepo.kernel.identifiers.IdentifierConverter;
028import org.fcrepo.kernel.impl.rdf.JcrRdfTools;
029import org.slf4j.Logger;
030
031import com.hp.hpl.jena.rdf.listeners.StatementListener;
032import com.hp.hpl.jena.rdf.model.Resource;
033import com.hp.hpl.jena.rdf.model.Statement;
034import com.hp.hpl.jena.rdf.model.RDFNode;
035import com.hp.hpl.jena.rdf.model.Property;
036import com.hp.hpl.jena.vocabulary.RDF;
037
038import java.util.ArrayList;
039import java.util.List;
040
041/**
042 * Listen to Jena statement events, and when the statement is changed in the
043 * graph store, make the change within JCR as well.
044 *
045 * @author awoods
046 */
047public class JcrPropertyStatementListener extends StatementListener {
048
049    private static final Logger LOGGER = getLogger(JcrPropertyStatementListener.class);
050
051    private final JcrRdfTools jcrRdfTools;
052
053    private final IdentifierConverter<Resource, FedoraResource> idTranslator;
054
055    private final List<Exception> exceptions;
056
057    /**
058     * Construct a statement listener within the given session
059     *
060     * @param idTranslator the id translator
061     * @param session the session
062     */
063    public JcrPropertyStatementListener(final IdentifierConverter<Resource, FedoraResource> idTranslator,
064                                        final Session session) {
065        this(idTranslator, new JcrRdfTools(idTranslator, session));
066    }
067
068    /**
069     * Construct a statement listener within the given session
070     *
071     * @param idTranslator the id translator
072     * @param jcrRdfTools the jcr rdf tools
073     */
074    public JcrPropertyStatementListener(final IdentifierConverter<Resource, FedoraResource> idTranslator,
075                                        final JcrRdfTools jcrRdfTools) {
076        super();
077        this.idTranslator = idTranslator;
078        this.jcrRdfTools = jcrRdfTools;
079        this.exceptions = new ArrayList<>();
080    }
081
082    /**
083     * When a statement is added to the graph, serialize it to a JCR property
084     *
085     * @param input the input statement
086     */
087    @Override
088    public void addedStatement(final Statement input) {
089        LOGGER.debug(">> added statement {}", input);
090
091        try {
092            final Resource subject = input.getSubject();
093
094            // if it's not about a node, ignore it.
095            if (!idTranslator.inDomain(subject) && !subject.isAnon()) {
096                LOGGER.error("subject ({}) is not in repository domain.", subject);
097                throw new MalformedRdfException(String.format(
098                    "Update RDF contains subject(s) (%s) not in the domain of this repository.", subject));
099            }
100
101            final Statement s = jcrRdfTools.skolemize(idTranslator, input);
102
103            final FedoraResource resource = idTranslator.convert(s.getSubject());
104
105            // special logic for handling rdf:type updates.
106            // if the object is an already-existing mixin, update
107            // the node's mixins. If it isn't, just treat it normally.
108            final Property property = s.getPredicate();
109            final RDFNode objectNode = s.getObject();
110            if (property.equals(RDF.type) && objectNode.isResource()) {
111                final Resource mixinResource = objectNode.asResource();
112                jcrRdfTools.addMixin(resource, mixinResource, input.getModel().getNsPrefixMap());
113                return;
114            }
115
116            jcrRdfTools.addProperty(resource, property, objectNode, input.getModel().getNsPrefixMap());
117        } catch (final RepositoryException | RepositoryRuntimeException e) {
118            exceptions.add(e);
119        }
120
121    }
122
123    /**
124     * When a statement is removed, remove it from the JCR properties
125     *
126     * @param s the given statement
127     */
128    @Override
129    public void removedStatement(final Statement s) {
130        LOGGER.trace(">> removed statement {}", s);
131
132        try {
133            final Resource subject = s.getSubject();
134
135            // if it's not about a node, we don't care.
136            if (!idTranslator.inDomain(subject)) {
137                LOGGER.error("subject ({}) is not in repository domain.", subject);
138                throw new MalformedRdfException(String.format(
139                    "Update RDF contains subject(s) (%s) not in the domain of this repository.", subject));
140            }
141
142            final FedoraResource resource = idTranslator.convert(subject);
143
144            // special logic for handling rdf:type updates.
145            // if the object is an already-existing mixin, update
146            // the node's mixins. If it isn't, just treat it normally.
147            final Property property = s.getPredicate();
148            final RDFNode objectNode = s.getObject();
149
150            if (property.equals(RDF.type) && objectNode.isResource()) {
151                final Resource mixinResource = objectNode.asResource();
152                jcrRdfTools.removeMixin(resource, mixinResource, s.getModel().getNsPrefixMap());
153                return;
154            }
155
156            jcrRdfTools.removeProperty(resource, property, objectNode, s.getModel().getNsPrefixMap());
157
158        } catch (final RepositoryException | RepositoryRuntimeException e) {
159            exceptions.add(e);
160        }
161
162    }
163
164    /**
165     * Assert that no exceptions were thrown while this listener was processing change
166     * @throws MalformedRdfException if malformed rdf exception occurred
167     * @throws javax.jcr.AccessDeniedException if access denied exception occurred
168     */
169    public void assertNoExceptions() throws MalformedRdfException, AccessDeniedException {
170        final StringBuilder sb = new StringBuilder();
171        for (Exception e : exceptions) {
172            sb.append(e.getMessage());
173            sb.append("\n");
174            if (e instanceof AccessDeniedException) {
175                throw new AccessDeniedException(sb.toString());
176            }
177        }
178        if (!exceptions.isEmpty()) {
179            throw new MalformedRdfException(sb.toString());
180        }
181    }
182}