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