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 java.util.stream.Collectors.joining; 019import static org.slf4j.LoggerFactory.getLogger; 020 021import javax.jcr.RepositoryException; 022import javax.jcr.Session; 023 024import org.fcrepo.kernel.api.exception.AccessDeniedException; 025import org.fcrepo.kernel.api.exception.ConstraintViolationException; 026import org.fcrepo.kernel.api.exception.IncorrectTripleSubjectException; 027import org.fcrepo.kernel.api.exception.MalformedRdfException; 028import org.fcrepo.kernel.api.exception.OutOfDomainSubjectException; 029import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 030import org.fcrepo.kernel.api.identifiers.IdentifierConverter; 031import org.fcrepo.kernel.api.models.FedoraResource; 032import org.fcrepo.kernel.modeshape.rdf.JcrRdfTools; 033 034import org.slf4j.Logger; 035 036import com.hp.hpl.jena.graph.Node; 037import com.hp.hpl.jena.rdf.listeners.StatementListener; 038import com.hp.hpl.jena.rdf.model.Resource; 039import com.hp.hpl.jena.rdf.model.Statement; 040import com.hp.hpl.jena.rdf.model.RDFNode; 041import com.hp.hpl.jena.rdf.model.Property; 042import com.hp.hpl.jena.vocabulary.RDF; 043 044import java.util.ArrayList; 045import java.util.List; 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 javax.jcr.AccessDeniedException e) { 125 throw new AccessDeniedException(e); 126 } catch (final RepositoryException | RepositoryRuntimeException e) { 127 exceptions.add(e); 128 } 129 130 } 131 132 /** 133 * When a statement is removed, remove it from the JCR properties 134 * 135 * @param s the given statement 136 */ 137 @Override 138 public void removedStatement(final Statement s) { 139 try { 140 // if it's not about the right kind of node, ignore it. 141 final Resource subject = s.getSubject(); 142 validateSubject(subject); 143 LOGGER.trace(">> removing statement {}", s); 144 145 final FedoraResource resource = idTranslator.convert(subject); 146 147 // special logic for handling rdf:type updates. 148 // if the object is an already-existing mixin, update 149 // the node's mixins. If it isn't, just treat it normally. 150 final Property property = s.getPredicate(); 151 final RDFNode objectNode = s.getObject(); 152 153 if (property.equals(RDF.type) && objectNode.isResource()) { 154 final Resource mixinResource = objectNode.asResource(); 155 jcrRdfTools.removeMixin(resource, mixinResource, s.getModel().getNsPrefixMap()); 156 return; 157 } 158 159 jcrRdfTools.removeProperty(resource, property, objectNode, s.getModel().getNsPrefixMap()); 160 } catch (final ConstraintViolationException e) { 161 throw e; 162 } catch (final RepositoryException | RepositoryRuntimeException e) { 163 exceptions.add(e); 164 } 165 166 } 167 168 /** 169 * If it's not the right kind of node, throw an appropriate unchecked exception. 170 * 171 * @param subject 172 */ 173 private void validateSubject(final Resource subject) { 174 final String subjectURI = subject.getURI(); 175 // blank nodes are okay 176 if (!subject.isAnon()) { 177 // hash URIs with the same base as the topic are okay 178 final int hashIndex = subjectURI.lastIndexOf("#"); 179 if (!(hashIndex > 0 && topic.getURI().equals(subjectURI.substring(0, hashIndex)))) { 180 // the topic itself is okay 181 if (!topic.equals(subject.asNode())) { 182 // it's a bad subject, but it could still be in-domain 183 if (idTranslator.inDomain(subject)) { 184 LOGGER.error("{} is not in the topic of this RDF, which is {}.", subject, topic); 185 throw new IncorrectTripleSubjectException(subject + 186 " is not in the topic of this RDF, which is " + topic); 187 } 188 // it's not even in the right domain! 189 LOGGER.error("subject ({}) is not in repository domain.", subject); 190 throw new OutOfDomainSubjectException(subject.asNode()); 191 } 192 } 193 } 194 } 195 196 /** 197 * Assert that no exceptions were thrown while this listener was processing change 198 */ 199 public void assertNoExceptions() { 200 if (!exceptions.isEmpty()) { 201 throw new MalformedRdfException(exceptions.stream().map(Exception::getMessage).collect(joining("\n"))); 202 } 203 } 204}