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.impl.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.models.FedoraResource; 025import org.fcrepo.kernel.exception.MalformedRdfException; 026import org.fcrepo.kernel.exception.RepositoryRuntimeException; 027import org.fcrepo.kernel.identifiers.IdentifierConverter; 028import org.fcrepo.kernel.impl.rdf.JcrRdfTools; 029import org.slf4j.Logger; 030 031import com.hp.hpl.jena.rdf.listeners.StatementListener; 032import com.hp.hpl.jena.rdf.model.Resource; 033import com.hp.hpl.jena.rdf.model.Statement; 034import com.hp.hpl.jena.rdf.model.RDFNode; 035import com.hp.hpl.jena.rdf.model.Property; 036import com.hp.hpl.jena.vocabulary.RDF; 037 038import java.util.ArrayList; 039import java.util.List; 040 041/** 042 * Listen to Jena statement events, and when the statement is changed in the 043 * graph store, make the change within JCR as well. 044 * 045 * @author awoods 046 */ 047public class JcrPropertyStatementListener extends StatementListener { 048 049 private static final Logger LOGGER = getLogger(JcrPropertyStatementListener.class); 050 051 private final JcrRdfTools jcrRdfTools; 052 053 private final IdentifierConverter<Resource, FedoraResource> idTranslator; 054 055 private final List<Exception> exceptions; 056 057 /** 058 * Construct a statement listener within the given session 059 * 060 * @param idTranslator the id translator 061 * @param session the session 062 */ 063 public JcrPropertyStatementListener(final IdentifierConverter<Resource, FedoraResource> idTranslator, 064 final Session session) { 065 this(idTranslator, new JcrRdfTools(idTranslator, session)); 066 } 067 068 /** 069 * Construct a statement listener within the given session 070 * 071 * @param idTranslator the id translator 072 * @param jcrRdfTools the jcr rdf tools 073 */ 074 public JcrPropertyStatementListener(final IdentifierConverter<Resource, FedoraResource> idTranslator, 075 final JcrRdfTools jcrRdfTools) { 076 super(); 077 this.idTranslator = idTranslator; 078 this.jcrRdfTools = jcrRdfTools; 079 this.exceptions = new ArrayList<>(); 080 } 081 082 /** 083 * When a statement is added to the graph, serialize it to a JCR property 084 * 085 * @param input the input statement 086 */ 087 @Override 088 public void addedStatement(final Statement input) { 089 LOGGER.debug(">> added statement {}", input); 090 091 try { 092 final Resource subject = input.getSubject(); 093 094 // if it's not about a node, ignore it. 095 if (!idTranslator.inDomain(subject) && !subject.isAnon()) { 096 LOGGER.error("subject ({}) is not in repository domain.", subject); 097 throw new MalformedRdfException(String.format( 098 "Update RDF contains subject(s) (%s) not in the domain of this repository.", subject)); 099 } 100 101 final Statement s = jcrRdfTools.skolemize(idTranslator, input); 102 103 final FedoraResource resource = idTranslator.convert(s.getSubject()); 104 105 // special logic for handling rdf:type updates. 106 // if the object is an already-existing mixin, update 107 // the node's mixins. If it isn't, just treat it normally. 108 final Property property = s.getPredicate(); 109 final RDFNode objectNode = s.getObject(); 110 if (property.equals(RDF.type) && objectNode.isResource()) { 111 final Resource mixinResource = objectNode.asResource(); 112 jcrRdfTools.addMixin(resource, mixinResource, input.getModel().getNsPrefixMap()); 113 return; 114 } 115 116 jcrRdfTools.addProperty(resource, property, objectNode, input.getModel().getNsPrefixMap()); 117 } catch (final RepositoryException | RepositoryRuntimeException e) { 118 exceptions.add(e); 119 } 120 121 } 122 123 /** 124 * When a statement is removed, remove it from the JCR properties 125 * 126 * @param s the given statement 127 */ 128 @Override 129 public void removedStatement(final Statement s) { 130 LOGGER.trace(">> removed statement {}", s); 131 132 try { 133 final Resource subject = s.getSubject(); 134 135 // if it's not about a node, we don't care. 136 if (!idTranslator.inDomain(subject)) { 137 LOGGER.error("subject ({}) is not in repository domain.", subject); 138 throw new MalformedRdfException(String.format( 139 "Update RDF contains subject(s) (%s) not in the domain of this repository.", subject)); 140 } 141 142 final FedoraResource resource = idTranslator.convert(subject); 143 144 // special logic for handling rdf:type updates. 145 // if the object is an already-existing mixin, update 146 // the node's mixins. If it isn't, just treat it normally. 147 final Property property = s.getPredicate(); 148 final RDFNode objectNode = s.getObject(); 149 150 if (property.equals(RDF.type) && objectNode.isResource()) { 151 final Resource mixinResource = objectNode.asResource(); 152 jcrRdfTools.removeMixin(resource, mixinResource, s.getModel().getNsPrefixMap()); 153 return; 154 } 155 156 jcrRdfTools.removeProperty(resource, property, objectNode, s.getModel().getNsPrefixMap()); 157 158 } catch (final RepositoryException | RepositoryRuntimeException e) { 159 exceptions.add(e); 160 } 161 162 } 163 164 /** 165 * Assert that no exceptions were thrown while this listener was processing change 166 * @throws MalformedRdfException if malformed rdf exception occurred 167 * @throws javax.jcr.AccessDeniedException if access denied exception occurred 168 */ 169 public void assertNoExceptions() throws MalformedRdfException, AccessDeniedException { 170 final StringBuilder sb = new StringBuilder(); 171 for (Exception e : exceptions) { 172 sb.append(e.getMessage()); 173 sb.append("\n"); 174 if (e instanceof AccessDeniedException) { 175 throw new AccessDeniedException(sb.toString()); 176 } 177 } 178 if (!exceptions.isEmpty()) { 179 throw new MalformedRdfException(sb.toString()); 180 } 181 } 182}