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; 048import java.util.Map; 049import java.util.concurrent.ConcurrentHashMap; 050 051/** 052 * Listen to Jena statement events, and when the statement is changed in the 053 * graph store, make the change within JCR as well. 054 * 055 * @author awoods 056 */ 057public class JcrPropertyStatementListener extends StatementListener { 058 059 private static final Logger LOGGER = getLogger(JcrPropertyStatementListener.class); 060 061 private enum Operation { 062 ADD, REMOVE 063 } 064 065 /* 066 * map statement hashcodes to the last successful operation on the statement 067 * to prevent large, redundant SPARQL solution sets from exhausting the heap 068 */ 069 private final Map<Statement,Operation> statements; 070 071 private final JcrRdfTools jcrRdfTools; 072 073 private final IdentifierConverter<Resource, FedoraResource> idTranslator; 074 075 private final List<Exception> exceptions; 076 077 private final Node topic; 078 079 /** 080 * Construct a statement listener within the given session 081 * 082 * @param idTranslator the id translator 083 * @param session the session 084 * @param topic the topic of the RDF statement 085 */ 086 public JcrPropertyStatementListener(final IdentifierConverter<Resource, FedoraResource> idTranslator, 087 final Session session, final Node topic) { 088 this(idTranslator, new JcrRdfTools(idTranslator, session), topic); 089 } 090 091 /** 092 * Construct a statement listener within the given session 093 * This listener is not reusable across requests. 094 * 095 * @param idTranslator the id translator 096 * @param jcrRdfTools the jcr rdf tools 097 * @param topic the topic of the RDF statement 098 */ 099 public JcrPropertyStatementListener(final IdentifierConverter<Resource, FedoraResource> idTranslator, 100 final JcrRdfTools jcrRdfTools, final Node topic) { 101 super(); 102 this.idTranslator = idTranslator; 103 this.jcrRdfTools = jcrRdfTools; 104 this.exceptions = new ArrayList<>(); 105 this.topic = topic; 106 this.statements = new ConcurrentHashMap<>(); 107 } 108 109 110 /** 111 * When a statement is added to the graph, serialize it to a JCR property 112 * 113 * @param input the input statement 114 */ 115 @Override 116 public void addedStatement(final Statement input) { 117 if (Operation.ADD == statements.get(input)) { 118 return; 119 } 120 try { 121 final Resource subject = input.getSubject(); 122 validateSubject(subject); 123 LOGGER.debug(">> adding statement {}", input); 124 125 final Statement s = jcrRdfTools.skolemize(idTranslator, input, topic.toString()); 126 127 final FedoraResource resource = idTranslator.convert(s.getSubject()); 128 final FedoraResource description = resource.getDescription(); 129 130 // special logic for handling rdf:type updates. 131 // if the object is an already-existing mixin, update 132 // the node's mixins. If it isn't, just treat it normally. 133 final Property property = s.getPredicate(); 134 final RDFNode objectNode = s.getObject(); 135 if (property.equals(RDF.type) && objectNode.isResource()) { 136 final Resource mixinResource = objectNode.asResource(); 137 jcrRdfTools.addMixin(description, mixinResource, input.getModel().getNsPrefixMap()); 138 statements.put(input, Operation.ADD); 139 return; 140 } 141 142 jcrRdfTools.addProperty(description, property, objectNode, input.getModel().getNsPrefixMap()); 143 statements.put(input, Operation.ADD); 144 } catch (final ConstraintViolationException e) { 145 throw e; 146 } catch (final javax.jcr.AccessDeniedException e) { 147 throw new AccessDeniedException(e); 148 } catch (final RepositoryException | RepositoryRuntimeException e) { 149 exceptions.add(e); 150 } 151 152 } 153 154 /** 155 * When a statement is removed, remove it from the JCR properties 156 * 157 * @param s the given statement 158 */ 159 @Override 160 public void removedStatement(final Statement s) { 161 if (Operation.REMOVE == statements.get(s)) { 162 return; 163 } 164 try { 165 // if it's not about the right kind of node, ignore it. 166 final Resource subject = s.getSubject(); 167 validateSubject(subject); 168 LOGGER.trace(">> removing statement {}", s); 169 170 final FedoraResource resource = idTranslator.convert(subject); 171 final FedoraResource description = resource.getDescription(); 172 173 // special logic for handling rdf:type updates. 174 // if the object is an already-existing mixin, update 175 // the node's mixins. If it isn't, just treat it normally. 176 final Property property = s.getPredicate(); 177 final RDFNode objectNode = s.getObject(); 178 179 if (property.equals(RDF.type) && objectNode.isResource()) { 180 final Resource mixinResource = objectNode.asResource(); 181 jcrRdfTools.removeMixin(description, mixinResource, s.getModel().getNsPrefixMap()); 182 statements.put(s, Operation.REMOVE); 183 return; 184 } 185 186 jcrRdfTools.removeProperty(description, property, objectNode, s.getModel().getNsPrefixMap()); 187 statements.put(s, Operation.REMOVE); 188 } catch (final ConstraintViolationException e) { 189 throw e; 190 } catch (final RepositoryException | RepositoryRuntimeException e) { 191 exceptions.add(e); 192 } 193 194 } 195 196 /** 197 * If it's not the right kind of node, throw an appropriate unchecked exception. 198 * 199 * @param subject to validate 200 */ 201 private void validateSubject(final Resource subject) { 202 final String subjectURI = subject.getURI(); 203 // blank nodes are okay 204 if (!subject.isAnon()) { 205 // hash URIs with the same base as the topic are okay 206 final int hashIndex = subjectURI.lastIndexOf("#"); 207 if (!(hashIndex > 0 && topic.getURI().equals(subjectURI.substring(0, hashIndex)))) { 208 // the topic itself is okay 209 if (!topic.equals(subject.asNode())) { 210 // it's a bad subject, but it could still be in-domain 211 if (idTranslator.inDomain(subject)) { 212 LOGGER.error("{} is not in the topic of this RDF, which is {}.", subject, topic); 213 throw new IncorrectTripleSubjectException(subject + 214 " is not in the topic of this RDF, which is " + topic); 215 } 216 // it's not even in the right domain! 217 LOGGER.error("subject ({}) is not in repository domain.", subject); 218 throw new OutOfDomainSubjectException(subject.asNode()); 219 } 220 } 221 } 222 } 223 224 /** 225 * Assert that no exceptions were thrown while this listener was processing change 226 */ 227 public void assertNoExceptions() { 228 if (!exceptions.isEmpty()) { 229 throw new MalformedRdfException(exceptions.stream().map(Exception::getMessage).collect(joining("\n"))); 230 } 231 } 232}