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.http.api.services; 019 020import static org.fcrepo.config.ServerManagedPropsMode.RELAXED; 021import static org.fcrepo.kernel.api.utils.RelaxedPropertiesHelper.checkTripleForDisallowed; 022import static org.slf4j.LoggerFactory.getLogger; 023 024import java.lang.reflect.Constructor; 025import java.util.ArrayList; 026import java.util.Collections; 027import java.util.List; 028 029import org.fcrepo.config.FedoraPropsConfig; 030import org.fcrepo.http.commons.api.rdf.HttpIdentifierConverter; 031import org.fcrepo.kernel.api.exception.ConstraintViolationException; 032import org.fcrepo.kernel.api.exception.MultipleConstraintViolationException; 033import org.fcrepo.kernel.api.exception.RelaxableServerManagedPropertyException; 034import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 035import org.fcrepo.kernel.api.exception.ServerManagedPropertyException; 036import org.fcrepo.kernel.api.exception.ServerManagedTypeException; 037 038import org.apache.jena.graph.Node; 039import org.apache.jena.graph.NodeFactory; 040import org.apache.jena.graph.Triple; 041import org.apache.jena.sparql.core.BasicPattern; 042import org.apache.jena.sparql.core.Quad; 043import org.apache.jena.sparql.modify.request.QuadAcc; 044import org.apache.jena.sparql.modify.request.QuadDataAcc; 045import org.apache.jena.sparql.modify.request.UpdateData; 046import org.apache.jena.sparql.modify.request.UpdateDataDelete; 047import org.apache.jena.sparql.modify.request.UpdateDataInsert; 048import org.apache.jena.sparql.modify.request.UpdateDeleteWhere; 049import org.apache.jena.sparql.modify.request.UpdateModify; 050import org.apache.jena.sparql.modify.request.UpdateVisitorBase; 051import org.apache.jena.sparql.syntax.Element; 052import org.apache.jena.sparql.syntax.ElementGroup; 053import org.apache.jena.sparql.syntax.ElementPathBlock; 054import org.apache.jena.update.Update; 055import org.apache.jena.update.UpdateFactory; 056import org.apache.jena.update.UpdateRequest; 057import org.slf4j.Logger; 058 059/** 060 * A special UpdateVisitor to translate Fedora URIs to internal FedoraIDs. 061 * @author whikloj 062 */ 063public class SparqlTranslateVisitor extends UpdateVisitorBase { 064 065 private static final Logger LOGGER = getLogger(SparqlTranslateVisitor.class); 066 067 private List<Update> newUpdates = new ArrayList<>(); 068 069 private HttpIdentifierConverter idTranslator; 070 071 private boolean isRelaxedMode; 072 073 public SparqlTranslateVisitor(final HttpIdentifierConverter identifierConverter, final FedoraPropsConfig config) { 074 idTranslator = identifierConverter; 075 isRelaxedMode = config.getServerManagedPropsMode().equals(RELAXED); 076 } 077 078 private List<ConstraintViolationException> exceptions = new ArrayList<>(); 079 080 @Override 081 public void visit(final UpdateDataInsert update) { 082 translateUpdate(update); 083 } 084 085 @Override 086 public void visit(final UpdateDataDelete update) { 087 translateUpdate(update); 088 } 089 090 @Override 091 public void visit(final UpdateDeleteWhere update) { 092 translateUpdate(update); 093 } 094 095 @Override 096 public void visit(final UpdateModify update) { 097 translateUpdate(update); 098 } 099 100 /** 101 * Get the new UpdateRequest based on the parsed Updates. 102 * @return the new update request object. 103 */ 104 public UpdateRequest getTranslatedRequest() { 105 final UpdateRequest newRequest = UpdateFactory.create(); 106 newUpdates.forEach(newRequest::add); 107 return newRequest; 108 } 109 110 /** 111 * Perform a translation of all the triples in an Update adding them to the internal list. 112 * @param update the update request to translate. 113 */ 114 private void translateUpdate(final Update update) { 115 final List<Quad> sourceQuads; 116 if (update instanceof UpdateDeleteWhere) { 117 sourceQuads = ((UpdateDeleteWhere)update).getQuads(); 118 } else { 119 sourceQuads = ((UpdateData) update).getQuads(); 120 } 121 final List<Quad> newQuads = translateQuads(sourceQuads); 122 assertNoExceptions(); 123 final Update newUpdate = makeUpdate(update.getClass(), newQuads); 124 newUpdates.add(newUpdate); 125 } 126 127 /** 128 * Perform a translation of all the triples in an UpdateModify request. 129 * @param update the update request to translate 130 */ 131 private void translateUpdate(final UpdateModify update) { 132 final UpdateModify newUpdate = new UpdateModify(); 133 final List<Quad> insertQuads = (update.hasInsertClause() ? translateQuads(update.getInsertQuads()) : 134 Collections.emptyList()); 135 final List<Quad> deleteQuads = (update.hasDeleteClause() ? translateQuads(update.getDeleteQuads()) : 136 Collections.emptyList()); 137 assertNoExceptions(); 138 139 insertQuads.forEach(q -> newUpdate.getInsertAcc().addQuad(q)); 140 deleteQuads.forEach(q -> newUpdate.getDeleteAcc().addQuad(q)); 141 142 final Element where = update.getWherePattern(); 143 final Element newElement = processElements(where); 144 newUpdate.setElement(newElement); 145 newUpdates.add(newUpdate); 146 } 147 148 private void assertNoExceptions() { 149 if (!exceptions.isEmpty()) { 150 throw new MultipleConstraintViolationException(exceptions); 151 } 152 } 153 154 /** 155 * Process triples inside the Element or return the element. 156 * @param element the element to translate. 157 * @return the translated or original element. 158 */ 159 private Element processElements(final Element element) { 160 if (element instanceof ElementGroup) { 161 final ElementGroup group = new ElementGroup(); 162 ((ElementGroup) element).getElements().forEach(e -> group.addElement(processElements(e))); 163 return group; 164 } else if (element instanceof ElementPathBlock) { 165 final BasicPattern basicPattern = new BasicPattern(); 166 final var tripleIter = ((ElementPathBlock) element).patternElts(); 167 tripleIter.forEachRemaining(t -> { 168 if (t.isTriple()) { 169 try { 170 checkTripleForDisallowed(t.asTriple()); 171 } catch (final ServerManagedPropertyException | ServerManagedTypeException exc) { 172 if (!isRelaxedMode) { 173 exceptions.add(exc); 174 return; 175 } 176 } 177 basicPattern.add(translateTriple(t.asTriple())); 178 } 179 }); 180 return new ElementPathBlock(basicPattern); 181 } 182 return element; 183 } 184 185 /** 186 * Perform the translation to a list of quads. 187 * @param quadsList the quads 188 * @return the translated list of quads. 189 */ 190 private List<Quad> translateQuads(final List<Quad> quadsList) { 191 final List<Quad> newQuads = new ArrayList<>(); 192 for (final Quad q : quadsList) { 193 try { 194 checkTripleForDisallowed(q.asTriple()); 195 } catch (final RelaxableServerManagedPropertyException exc) { 196 if (!isRelaxedMode) { 197 // Swallow these exceptions to throw together later. 198 exceptions.add(exc); 199 continue; 200 } 201 } catch (final ServerManagedTypeException | ServerManagedPropertyException exc) { 202 exceptions.add(exc); 203 continue; 204 } 205 final Node subject = translateId(q.getSubject()); 206 final Node object = translateId(q.getObject()); 207 final Quad quad = new Quad(q.getGraph(), subject, q.getPredicate(), object); 208 LOGGER.trace("Translated quad is: {}", quad); 209 newQuads.add(quad); 210 } 211 return newQuads; 212 } 213 214 /** 215 * Translate the subject and object of a triple from external URIs to internal IDs. 216 * @param triple the triple to translate 217 * @return the translated triple. 218 */ 219 private Triple translateTriple(final Triple triple) { 220 final Node subject = translateId(triple.getSubject()); 221 final Node object = translateId(triple.getObject()); 222 return Triple.create(subject, triple.getPredicate(), object); 223 } 224 225 /** 226 * Quads insert/delete data statements don't contain variables and use QuadDataAcc to accumulate, 227 * insert {} delete {} where {} statements can't contain variables and use QuadAcc to accumulate. This function 228 * simplifies the creation of the eventual Update. 229 * @param updateClass the class of Update we are starting with. 230 * @param quadList the list of Quads to generate the above class with. 231 * @return a subclass of Update with the provided Quads. 232 */ 233 private Update makeUpdate(final Class<? extends Update> updateClass, final List<Quad> quadList) { 234 try { 235 if (updateClass.equals(UpdateDeleteWhere.class)) { 236 final QuadAcc quadAcc = new QuadAcc(); 237 quadList.forEach(quadAcc::addQuad); 238 return new UpdateDeleteWhere(quadAcc); 239 } else { 240 final QuadDataAcc quadsAcc = new QuadDataAcc(); 241 quadList.forEach(quadsAcc::addQuad); 242 final Constructor<? extends Update> update = updateClass.getConstructor(QuadDataAcc.class); 243 return update.newInstance(quadsAcc); 244 } 245 } catch (final ReflectiveOperationException exc) { 246 LOGGER.warn("Could not find constructor UpdateRequest"); 247 throw new RepositoryRuntimeException("Could not find constructor UpdateRequest", exc); 248 } 249 } 250 251 /** 252 * If the node is a URI with the external domain translate it, otherwise leave it alone 253 * @param externalNode the node to translate 254 * @return the original or translated node. 255 */ 256 private Node translateId(final Node externalNode) { 257 if (externalNode.isURI()) { 258 final String externalId = externalNode.getURI(); 259 final String newUri = idTranslator.translateUri(externalId); 260 return NodeFactory.createURI(newUri); 261 } 262 return externalNode; 263 } 264 265}