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 org.apache.jena.graph.Node; 039import org.apache.jena.rdf.listeners.StatementListener; 040import org.apache.jena.rdf.model.Resource; 041import org.apache.jena.rdf.model.Statement; 042import org.apache.jena.rdf.model.RDFNode; 043import org.apache.jena.rdf.model.Property; 044import org.apache.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}