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}