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}