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 */ 018 019package org.fcrepo.http.api.services; 020 021import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; 022import static org.apache.jena.rdf.model.ResourceFactory.createProperty; 023import static org.apache.jena.riot.RDFLanguages.contentTypeToLang; 024import static org.fcrepo.config.ServerManagedPropsMode.STRICT; 025import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate; 026import static org.fcrepo.kernel.api.RdfLexicon.restrictedType; 027import static org.fcrepo.kernel.api.utils.RelaxedPropertiesHelper.checkTripleForDisallowed; 028import static org.slf4j.LoggerFactory.getLogger; 029 030import java.io.InputStream; 031import java.util.ArrayList; 032import java.util.List; 033 034import javax.inject.Inject; 035import javax.ws.rs.BadRequestException; 036import javax.ws.rs.core.MediaType; 037 038import org.fcrepo.config.FedoraPropsConfig; 039import org.fcrepo.http.commons.api.rdf.HttpIdentifierConverter; 040import org.fcrepo.kernel.api.RdfStream; 041import org.fcrepo.kernel.api.exception.ConstraintViolationException; 042import org.fcrepo.kernel.api.exception.MalformedRdfException; 043import org.fcrepo.kernel.api.exception.MultipleConstraintViolationException; 044import org.fcrepo.kernel.api.exception.RelaxableServerManagedPropertyException; 045import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 046import org.fcrepo.kernel.api.exception.ServerManagedPropertyException; 047import org.fcrepo.kernel.api.exception.ServerManagedTypeException; 048import org.fcrepo.kernel.api.exception.UnsupportedMediaTypeException; 049import org.fcrepo.kernel.api.identifiers.FedoraId; 050import org.fcrepo.kernel.api.rdf.DefaultRdfStream; 051 052import org.apache.jena.atlas.RuntimeIOException; 053import org.apache.jena.graph.Node; 054import org.apache.jena.graph.NodeFactory; 055import org.apache.jena.graph.Triple; 056import org.apache.jena.rdf.model.Model; 057import org.apache.jena.rdf.model.RDFNode; 058import org.apache.jena.rdf.model.Statement; 059import org.apache.jena.rdf.model.StmtIterator; 060import org.apache.jena.rdf.model.impl.StatementImpl; 061import org.apache.jena.riot.Lang; 062import org.apache.jena.riot.RiotException; 063import org.apache.jena.update.Update; 064import org.apache.jena.update.UpdateFactory; 065import org.apache.jena.update.UpdateRequest; 066import org.slf4j.Logger; 067import org.springframework.stereotype.Component; 068 069import com.fasterxml.jackson.core.JsonParseException; 070 071/** 072 * A service that will translate the resourceURI to Fedora ID in the Rdf InputStream 073 * 074 * @author bseeger 075 * @author bbpennel 076 * @since 2019-11-07 077 */ 078@Component 079public class HttpRdfService { 080 081 private static final Logger log = getLogger(HttpRdfService.class); 082 083 @Inject 084 private FedoraPropsConfig fedoraPropsConfig; 085 086 /** 087 * Convert internal IDs to external URIs 088 * @param extResourceId The external URI of the resource. 089 * @param stream The RDF stream to be translated. 090 * @param idTranslator The identifier converter. 091 * @return a converted RDF Stream. 092 */ 093 public RdfStream bodyToExternalStream(final String extResourceId, final RdfStream stream, 094 final HttpIdentifierConverter idTranslator) { 095 return new DefaultRdfStream(NodeFactory.createURI(extResourceId), stream.map(t -> { 096 final Node subject = makeExternalNode(t.getSubject(), idTranslator); 097 final Node object = makeExternalNode(t.getObject(), idTranslator); 098 return new Triple(subject, t.getPredicate(), object); 099 })); 100 } 101 102 /** 103 * Return a converted or original resource. 104 * @param resource The Node to be checked. 105 * @param identifierConverter A identifier converter. 106 * @return The resulting node. 107 */ 108 private Node makeExternalNode(final Node resource, final HttpIdentifierConverter identifierConverter) { 109 if (resource.isURI() && identifierConverter.inInternalDomain(resource.toString())) { 110 return NodeFactory.createURI(identifierConverter.toExternalId(resource.toString())); 111 } else { 112 return resource; 113 } 114 } 115 116 /** 117 * Parse the request body to a Model, with the URI to Fedora ID translations done. 118 * 119 * @param extResourceId the external ID of the Fedora resource 120 * @param stream the input stream containing the RDF 121 * @param contentType the media type of the RDF 122 * @param idTranslator the identifier convert 123 * @param lenientHandling whether the request included a handling=lenient prefer header. 124 * @return RdfStream containing triples from request body, with Fedora IDs in them 125 * @throws MalformedRdfException in case rdf json cannot be parsed 126 * @throws BadRequestException in the case where the RDF syntax is bad 127 */ 128 public Model bodyToInternalModel(final FedoraId extResourceId, final InputStream stream, 129 final MediaType contentType, final HttpIdentifierConverter idTranslator, 130 final boolean lenientHandling) 131 throws RepositoryRuntimeException, BadRequestException { 132 final List<ConstraintViolationException> exceptions = new ArrayList<>(); 133 final String externalURI = idTranslator.toExternalId(extResourceId.getFullId()); 134 final Model model = parseBodyAsModel(stream, contentType, externalURI); 135 final List<Statement> insertStatements = new ArrayList<>(); 136 final StmtIterator stmtIterator = model.listStatements(); 137 138 while (stmtIterator.hasNext()) { 139 final Statement stmt = stmtIterator.nextStatement(); 140 if (lenientHandling && stmtIsServerManaged(stmt) && 141 fedoraPropsConfig.getServerManagedPropsMode().equals(STRICT)) { 142 // Remove any statement that touches a server managed property or namespace. 143 stmtIterator.remove(); 144 } else { 145 try { 146 checkForDisallowedRdf(stmt); 147 } catch (final RelaxableServerManagedPropertyException exc) { 148 if (fedoraPropsConfig.getServerManagedPropsMode().equals(STRICT)) { 149 exceptions.add(exc); 150 continue; 151 } 152 } catch (final ServerManagedTypeException | ServerManagedPropertyException exc) { 153 if (lenientHandling) { 154 // Remove the invalid statement because client specified lenient handling. 155 stmtIterator.remove(); 156 } else { 157 exceptions.add(exc); 158 } 159 continue; 160 } 161 if (stmt.getSubject().isURIResource()) { 162 final String originalSubj = stmt.getSubject().getURI(); 163 final String subj = idTranslator.translateUri(originalSubj); 164 165 RDFNode obj = stmt.getObject(); 166 if (stmt.getObject().isURIResource()) { 167 final String objString = stmt.getObject().asResource().getURI(); 168 final String objUri = idTranslator.translateUri(objString); 169 obj = model.getResource(objUri); 170 } 171 172 if (!subj.equals(originalSubj) || !obj.equals(stmt.getObject())) { 173 insertStatements.add(new StatementImpl(model.getResource(subj), stmt.getPredicate(), obj)); 174 175 stmtIterator.remove(); 176 } 177 } else { 178 log.debug("Subject {} is not a URI resource, skipping", stmt.getSubject()); 179 } 180 } 181 } 182 183 if (!exceptions.isEmpty()) { 184 throw new MultipleConstraintViolationException(exceptions); 185 } 186 187 model.add(insertStatements); 188 189 log.debug("Model: {}", model); 190 return model; 191 } 192 193 /** 194 * Takes a PATCH request body and translates any subjects and objects that are in the domain of the repository 195 * to use internal IDs. 196 * @param resourceId the internal ID of the current resource. 197 * @param requestBody the request body. 198 * @param idTranslator an ID converter for the current context. 199 * @return the converted PATCH request. 200 */ 201 public String patchRequestToInternalString(final FedoraId resourceId, final String requestBody, 202 final HttpIdentifierConverter idTranslator) { 203 final String externalURI = idTranslator.toExternalId(resourceId.getFullId()); 204 final UpdateRequest request = UpdateFactory.create(requestBody, externalURI); 205 final List<Update> updates = request.getOperations(); 206 final SparqlTranslateVisitor visitor = new SparqlTranslateVisitor(idTranslator, fedoraPropsConfig); 207 for (final Update update : updates) { 208 update.visit(visitor); 209 } 210 return visitor.getTranslatedRequest().toString(); 211 } 212 213 /** 214 * Parse the request body as a Model. 215 * 216 * @param requestBodyStream rdf request body 217 * @param contentType content type of body 218 * @param extResourceId the external ID of the Fedora resource 219 * @return Model containing triples from request body 220 * @throws MalformedRdfException in case rdf json cannot be parsed 221 * @throws BadRequestException in the case where the RDF syntax is bad 222 */ 223 protected static Model parseBodyAsModel(final InputStream requestBodyStream, 224 final MediaType contentType, 225 final String extResourceId) throws BadRequestException, 226 RepositoryRuntimeException { 227 228 if (requestBodyStream == null) { 229 return null; 230 } 231 232 // The 'contentTypeToLang()' method will not accept 'charset' parameters 233 String contentTypeWithoutCharset = contentType.toString(); 234 if (contentType.getParameters().containsKey("charset")) { 235 contentTypeWithoutCharset = contentType.getType() + "/" + contentType.getSubtype(); 236 } 237 238 final Lang format = contentTypeToLang(contentTypeWithoutCharset); 239 if (format == null) { 240 // No valid RDF format for the mimeType. 241 throw new UnsupportedMediaTypeException("Media type " + contentTypeWithoutCharset + " is not a valid RDF " + 242 "format"); 243 } 244 try { 245 final Model inputModel = createDefaultModel(); 246 inputModel.read(requestBodyStream, extResourceId, format.getName().toUpperCase()); 247 return inputModel; 248 } catch (final RiotException e) { 249 throw new BadRequestException("RDF was not parsable: " + e.getMessage(), e); 250 251 } catch (final RuntimeIOException e) { 252 if (e.getCause() instanceof JsonParseException) { 253 final var cause = e.getCause(); 254 throw new MalformedRdfException(cause.getMessage(), cause); 255 } 256 throw new RepositoryRuntimeException(e.getMessage(), e); 257 } 258 } 259 260 /** 261 * Checks if the RDF contains any disallowed statements. 262 * @param statement a statement from the incoming RDF. 263 */ 264 private static void checkForDisallowedRdf(final Statement statement) { 265 checkTripleForDisallowed(statement.asTriple()); 266 } 267 268 /** 269 * Does the statement's triple touch any server managed properties / namespaces. 270 * ie. 271 * - has a rdf:type with an object which is in a managed namespace 272 * - has a predicate which is in a managed namespace. 273 * 274 * @param statement the statement to check 275 * @return Return true if this does touch a server managed property or namespace. 276 */ 277 private boolean stmtIsServerManaged(final Statement statement) { 278 final Triple triple = statement.asTriple(); 279 return restrictedType.test(triple) || isManagedPredicate.test(createProperty(triple.getPredicate().getURI())); 280 } 281}