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.RepositoryException;
021import javax.jcr.Session;
022
023import com.google.common.base.Joiner;
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<String> exceptions;
056
057    /**
058     * Construct a statement listener within the given session
059     *
060     * @param idTranslator
061     * @param 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
072     */
073    public JcrPropertyStatementListener(final IdentifierConverter<Resource, FedoraResource> idTranslator,
074                                        final JcrRdfTools jcrRdfTools) {
075        super();
076        this.idTranslator = idTranslator;
077        this.jcrRdfTools = jcrRdfTools;
078        this.exceptions = new ArrayList<>();
079    }
080
081    /**
082     * When a statement is added to the graph, serialize it to a JCR property
083     *
084     * @param input
085     */
086    @Override
087    public void addedStatement(final Statement input) {
088        LOGGER.debug(">> added statement {}", input);
089
090        try {
091            final Resource subject = input.getSubject();
092
093            // if it's not about a node, ignore it.
094            if (!idTranslator.inDomain(subject) && !subject.isAnon()) {
095                return;
096            }
097
098            final Statement s = jcrRdfTools.skolemize(idTranslator, input);
099
100            final FedoraResource resource = idTranslator.convert(s.getSubject());
101
102            // special logic for handling rdf:type updates.
103            // if the object is an already-existing mixin, update
104            // the node's mixins. If it isn't, just treat it normally.
105            final Property property = s.getPredicate();
106            final RDFNode objectNode = s.getObject();
107            if (property.equals(RDF.type) && objectNode.isResource()) {
108                final Resource mixinResource = objectNode.asResource();
109                jcrRdfTools.addMixin(resource, mixinResource, input.getModel().getNsPrefixMap());
110                return;
111            }
112
113            jcrRdfTools.addProperty(resource, property, objectNode, input.getModel().getNsPrefixMap());
114        } catch (final RepositoryException | RepositoryRuntimeException e) {
115            exceptions.add(e.getMessage());
116        }
117
118    }
119
120    /**
121     * When a statement is removed, remove it from the JCR properties
122     *
123     * @param s
124     */
125    @Override
126    public void removedStatement(final Statement s) {
127        LOGGER.trace(">> removed statement {}", s);
128
129        try {
130            final Resource subject = s.getSubject();
131
132            // if it's not about a node, we don't care.
133            if (!idTranslator.inDomain(subject)) {
134                return;
135            }
136
137            final FedoraResource resource = idTranslator.convert(subject);
138
139            // special logic for handling rdf:type updates.
140            // if the object is an already-existing mixin, update
141            // the node's mixins. If it isn't, just treat it normally.
142            final Property property = s.getPredicate();
143            final RDFNode objectNode = s.getObject();
144
145            if (property.equals(RDF.type) && objectNode.isResource()) {
146                final Resource mixinResource = objectNode.asResource();
147                try {
148                    jcrRdfTools.removeMixin(resource, mixinResource, s.getModel().getNsPrefixMap());
149                } catch (final RepositoryException e) {
150                    // TODO
151                }
152                return;
153            }
154
155            jcrRdfTools.removeProperty(resource, property, objectNode, s.getModel().getNsPrefixMap());
156
157        } catch (final RepositoryException | RepositoryRuntimeException e) {
158            exceptions.add(e.getMessage());
159        }
160
161    }
162
163    /**
164     * Assert that no exceptions were thrown while this listener was processing change
165     */
166    public void assertNoExceptions() throws MalformedRdfException {
167        if (!exceptions.isEmpty()) {
168            throw new MalformedRdfException(Joiner.on("\n").join(exceptions));
169        }
170    }
171}