/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jena.fuseki.servlets;
import static java.lang.String.format ;
import java.io.IOException ;
import java.io.InputStream ;
import java.util.Map ;
import java.util.Map.Entry ;
import org.apache.jena.atlas.io.IO ;
import org.apache.jena.atlas.web.ContentType ;
import org.apache.jena.fuseki.FusekiLib ;
import org.apache.jena.fuseki.HttpNames ;
import org.apache.jena.graph.Graph ;
import org.apache.jena.riot.Lang ;
import org.apache.jena.riot.RDFLanguages ;
import org.apache.jena.riot.RiotException ;
import org.apache.jena.riot.WebContent ;
import org.apache.jena.riot.system.StreamRDF ;
import org.apache.jena.riot.system.StreamRDFLib ;
import org.apache.jena.sparql.graph.GraphFactory ;
import org.apache.jena.web.HttpSC ;
/** The WRITE operations added to the READ operations */
public class SPARQL_REST_RW extends SPARQL_REST_R
{
public SPARQL_REST_RW()
{ super() ; }
@Override
protected void doOptions(HttpAction action)
{
setCommonHeadersForOptions(action.response) ;
action.response.setHeader(HttpNames.hAllow, "GET,HEAD,OPTIONS,PUT,DELETE,POST");
action.response.setHeader(HttpNames.hContentLengh, "0") ;
success(action) ;
}
@Override
protected void doDelete(HttpAction action)
{
action.beginWrite() ;
try {
Target target = determineTarget(action) ;
if ( log.isDebugEnabled() )
log.debug("DELETE->"+target) ;
boolean existedBefore = target.exists() ;
if ( ! existedBefore)
{
// commit, not abort, because locking "transactions" don't support abort.
action.commit() ;
errorNotFound("No such graph: "+target.name) ;
}
deleteGraph(action) ;
action.commit() ;
}
finally { action.endWrite() ; }
ServletBase.successNoContent(action) ;
}
@Override
protected void doPut(HttpAction action) { doPutPost(action, true) ; }
@Override
protected void doPost(HttpAction action) { doPutPost(action, false) ; }
private void doPutPost(HttpAction action, boolean overwrite) {
ContentType ct = FusekiLib.getContentType(action) ;
if ( ct == null )
errorBadRequest("No Content-Type:") ;
// Helper case - if it's a possible HTTP file upload, pretend that's the action.
if ( WebContent.contentTypeMultipartFormData.equalsIgnoreCase(ct.getContentType()) ) {
String base = wholeRequestURL(action.request) ;
SPARQL_Upload.upload(action, base) ;
return ;
}
if ( WebContent.matchContentType(WebContent.ctMultipartMixed, ct) )
error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "multipart/mixed not supported") ;
boolean existedBefore = false ;
if ( action.isTransactional() )
existedBefore = addDataIntoTxn(action, overwrite) ;
else
existedBefore = addDataIntoNonTxn(action, overwrite) ;
if ( existedBefore )
ServletBase.successNoContent(action) ;
else
ServletBase.successCreated(action) ;
}
/** Directly add data in a transaction.
* Assumes recovery from parse errors by transaction abort.
* Return whether the target existed before.
* @param action
* @param cleanDest Whether to remove data first (true = PUT, false = POST)
* @return whether the target existed beforehand
*/
protected static boolean addDataIntoTxn(HttpAction action, boolean overwrite) {
action.beginWrite();
Target target = determineTarget(action) ;
boolean existedBefore = false ;
try {
if ( log.isDebugEnabled() )
log.debug(" ->"+target) ;
existedBefore = target.exists() ;
Graph g = target.graph() ;
if ( overwrite && existedBefore )
clearGraph(target) ;
StreamRDF sink = StreamRDFLib.graph(g) ;
incomingData(action, sink);
action.commit() ;
return existedBefore ;
} catch (RiotException ex) {
// Parse error
action.abort() ;
errorBadRequest(ex.getMessage()) ;
return existedBefore ;
} catch (Exception ex) {
// Something else went wrong. Backout.
action.abort() ;
errorOccurred(ex.getMessage()) ;
return existedBefore ;
} finally {
action.endWrite() ;
}
}
/** Add data where the destination does not support full transactions.
* In particular, with no abort, and actions probably going to the real storage
* parse errors can lead to partial updates. Instead, parse to a temporary
* graph, then insert that data.
* @param action
* @param cleanDest Whether to remove data first (true = PUT, false = POST)
* @return whether the target existed beforehand.
*/
protected static boolean addDataIntoNonTxn(HttpAction action, boolean overwrite) {
Graph graphTmp = GraphFactory.createGraphMem() ;
StreamRDF dest = StreamRDFLib.graph(graphTmp) ;
try { incomingData(action, dest); }
catch (RiotException ex) {
errorBadRequest(ex.getMessage()) ;
return false ;
}
// Now insert into dataset
action.beginWrite() ;
Target target = determineTarget(action) ;
boolean existedBefore = false ;
try {
if ( log.isDebugEnabled() )
log.debug(" ->"+target) ;
existedBefore = target.exists() ;
if ( overwrite && existedBefore )
clearGraph(target) ;
FusekiLib.addDataInto(graphTmp, target.dsg, target.graphName) ;
action.commit() ;
return existedBefore ;
} catch (Exception ex) {
// We parsed into a temporary graph so an exception at this point
// is not because of a parse error.
// We're in the non-transactional branch, this probably will not work
// but it might and there is no harm safely trying.
try { action.abort() ; } catch (Exception ex2) {}
errorOccurred(ex.getMessage()) ;
return existedBefore ;
} finally { action.endWrite() ; }
}
private static void incomingData(HttpAction action, StreamRDF dest) {
String base = wholeRequestURL(action.request) ;
ContentType ct = FusekiLib.getContentType(action) ;
Lang lang = RDFLanguages.contentTypeToLang(ct.getContentType()) ;
if ( lang == null ) {
errorBadRequest("Unknown content type for triples: " + ct) ;
return ;
}
InputStream input = null ;
try { input = action.request.getInputStream() ; }
catch (IOException ex) { IO.exception(ex) ; }
int len = action.request.getContentLength() ;
if ( action.verbose ) {
if ( len >= 0 )
log.info(format("[%d] Body: Content-Length=%d, Content-Type=%s, Charset=%s => %s", action.id, len,
ct.getContentType(), ct.getCharset(), lang.getName())) ;
else
log.info(format("[%d] Body: Content-Type=%s, Charset=%s => %s", action.id, ct.getContentType(),
ct.getCharset(), lang.getName())) ;
}
parse(action, dest, input, lang, base) ;
}
protected static void deleteGraph(HttpAction action) {
Target target = determineTarget(action) ;
if ( target.isDefault )
target.graph().clear() ;
else
action.getActiveDSG().removeGraph(target.graphName) ;
}
protected static void clearGraph(Target target) {
Graph g = target.graph() ;
g.clear() ;
Map<String, String> pm = g.getPrefixMapping().getNsPrefixMap() ;
for ( Entry<String, String> e : pm.entrySet() )
g.getPrefixMapping().removeNsPrefix(e.getKey()) ;
}
}