/**
* 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.mgt;
import static java.lang.String.format ;
import java.io.IOException ;
import java.io.InputStream ;
import java.io.OutputStream ;
import java.io.StringReader ;
import java.util.HashMap ;
import java.util.Iterator ;
import java.util.List ;
import java.util.Map ;
import javax.servlet.ServletOutputStream ;
import javax.servlet.http.HttpServletRequest ;
import org.apache.commons.lang3.StringUtils;
import org.apache.jena.atlas.io.IO ;
import org.apache.jena.atlas.json.JsonBuilder ;
import org.apache.jena.atlas.json.JsonValue ;
import org.apache.jena.atlas.lib.FileOps ;
import org.apache.jena.atlas.lib.InternalErrorException ;
import org.apache.jena.atlas.lib.StrUtils ;
import org.apache.jena.atlas.web.ContentType ;
import org.apache.jena.datatypes.xsd.XSDDatatype ;
import org.apache.jena.fuseki.FusekiLib ;
import org.apache.jena.fuseki.build.* ;
import org.apache.jena.fuseki.server.* ;
import org.apache.jena.fuseki.servlets.* ;
import org.apache.jena.graph.Node ;
import org.apache.jena.graph.NodeFactory ;
import org.apache.jena.query.Dataset ;
import org.apache.jena.query.ReadWrite ;
import org.apache.jena.rdf.model.* ;
import org.apache.jena.riot.Lang ;
import org.apache.jena.riot.RDFDataMgr ;
import org.apache.jena.riot.RDFLanguages ;
import org.apache.jena.riot.WebContent ;
import org.apache.jena.riot.system.StreamRDF ;
import org.apache.jena.riot.system.StreamRDFLib ;
import org.apache.jena.shared.uuid.JenaUUID ;
import org.apache.jena.sparql.core.DatasetGraph ;
import org.apache.jena.sparql.core.Quad ;
import org.apache.jena.sparql.util.FmtUtils ;
import org.apache.jena.tdb.transaction.DatasetGraphTransaction ;
import org.apache.jena.update.UpdateAction ;
import org.apache.jena.update.UpdateFactory ;
import org.apache.jena.update.UpdateRequest ;
import org.apache.jena.web.HttpSC ;
public class ActionDatasets extends ActionContainerItem {
private static final long serialVersionUID = 5171975468398320835L;
private static Dataset system = SystemState.getDataset() ;
private static DatasetGraphTransaction systemDSG = SystemState.getDatasetGraph() ;
static private Property pServiceName = FusekiVocab.pServiceName ;
static private Property pStatus = FusekiVocab.pStatus ;
private static final String paramDatasetName = "dbName" ;
private static final String paramDatasetType = "dbType" ;
private static final String tDatabasetTDB = "tdb" ;
private static final String tDatabasetMem = "mem" ;
public ActionDatasets() { super() ; }
// ---- GET : return details of dataset or datasets.
@Override
protected JsonValue execGetContainer(HttpAction action) {
action.log.info(format("[%d] GET datasets", action.id)) ;
JsonBuilder builder = new JsonBuilder() ;
builder.startObject("D") ;
builder.key(JsonConst.datasets) ;
JsonDescription.arrayDatasets(builder, action.getDataAccessPointRegistry());
builder.finishObject("D") ;
return builder.build() ;
}
@Override
protected JsonValue execGetItem(HttpAction action) {
action.log.info(format("[%d] GET dataset %s", action.id, action.getDatasetName())) ;
JsonBuilder builder = new JsonBuilder() ;
DataAccessPoint dsDesc = action.getDataAccessPointRegistry().get(action.getDatasetName()) ;
if ( dsDesc == null )
ServletOps.errorNotFound("Not found: dataset "+action.getDatasetName());
JsonDescription.describe(builder, dsDesc) ;
return builder.build() ;
}
// ---- POST
@Override
protected JsonValue execPostContainer(HttpAction action) {
JenaUUID uuid = JenaUUID.generate() ;
String newURI = uuid.asURI() ;
Node gn = NodeFactory.createURI(newURI) ;
DatasetDescriptionRegistry registry = FusekiServer.registryForBuild() ;
ContentType ct = FusekiLib.getContentType(action) ;
boolean committed = false ;
// Also acts as a concurrency lock
system.begin(ReadWrite.WRITE) ;
String systemFileCopy = null ;
String configFile = null ;
try {
// Where to build the templated service/database.
Model model = ModelFactory.createDefaultModel() ;
StreamRDF dest = StreamRDFLib.graph(model.getGraph()) ;
if ( WebContent.isHtmlForm(ct) )
assemblerFromForm(action, dest) ;
else if ( WebContent.isMultiPartForm(ct) )
assemblerFromUpload(action, dest) ;
else
assemblerFromBody(action, dest) ;
// ----
// Keep a persistent copy immediately. This is not used for
// anything other than being "for the record".
systemFileCopy = FusekiServer.dirFileArea.resolve(uuid.asString()).toString() ;
try ( OutputStream outCopy = IO.openOutputFile(systemFileCopy) ) {
RDFDataMgr.write(outCopy, model, Lang.TURTLE) ;
}
// ----
// Process configuration.
Statement stmt = getOne(model, null, pServiceName, null) ;
if ( stmt == null ) {
StmtIterator sIter = model.listStatements(null, pServiceName, (RDFNode)null ) ;
if ( ! sIter.hasNext() )
ServletOps.errorBadRequest("No name given in description of Fuseki service") ;
sIter.next() ;
if ( sIter.hasNext() )
ServletOps.errorBadRequest("Multiple names given in description of Fuseki service") ;
throw new InternalErrorException("Inconsistent: getOne didn't fail the second time") ;
}
if ( ! stmt.getObject().isLiteral() )
ServletOps.errorBadRequest("Found "+FmtUtils.stringForRDFNode(stmt.getObject())+" : Service names are strings, then used to build the external URI") ;
Resource subject = stmt.getSubject() ;
Literal object = stmt.getObject().asLiteral() ;
if ( object.getDatatype() != null && ! object.getDatatype().equals(XSDDatatype.XSDstring) )
action.log.warn(format("[%d] Service name '%s' is not a string", action.id, FmtUtils.stringForRDFNode(object)));
String datasetPath ;
{ // Check the name provided.
String datasetName = object.getLexicalForm() ;
// ---- Check and canonicalize name.
if ( datasetName.isEmpty() )
ServletOps.error(HttpSC.BAD_REQUEST_400, "Empty dataset name") ;
if ( StringUtils.isBlank(datasetName) )
ServletOps.error(HttpSC.BAD_REQUEST_400, format("Whitespace dataset name: '%s'", datasetName)) ;
if ( datasetName.contains(" ") )
ServletOps.error(HttpSC.BAD_REQUEST_400, format("Bad dataset name (contains spaces) '%s'",datasetName)) ;
if ( datasetName.equals("/") )
ServletOps.error(HttpSC.BAD_REQUEST_400, format("Bad dataset name '%s'",datasetName)) ;
datasetPath = DataAccessPoint.canonical(datasetName) ;
}
action.log.info(format("[%d] Create database : name = %s", action.id, datasetPath)) ;
// System.err.println("'"+datasetPath+"'") ;
// DataAccessPointRegistry.get().forEach((s,dap)->System.err.println("'"+s+"'"));
// ---- Check whether it already exists
if ( action.getDataAccessPointRegistry().isRegistered(datasetPath) )
// And abort.
ServletOps.error(HttpSC.CONFLICT_409, "Name already registered "+datasetPath) ;
configFile = FusekiEnv.generateConfigurationFilename(datasetPath) ;
List<String> existing = FusekiEnv.existingConfigurationFile(datasetPath) ;
if ( ! existing.isEmpty() )
ServletOps.error(HttpSC.CONFLICT_409, "Configuration file for '"+datasetPath+"' already exists") ;
// Write to configuration directory.
try ( OutputStream outCopy = IO.openOutputFile(configFile) ) {
RDFDataMgr.write(outCopy, model, Lang.TURTLE) ;
}
// Currently do nothing with the system database.
// In the future ... maybe ...
// Model modelSys = system.getNamedModel(gn.getURI()) ;
// modelSys.removeAll(null, pStatus, null) ;
// modelSys.add(subject, pStatus, FusekiVocab.stateActive) ;
// Need to be in Resource space at this point.
DataAccessPoint ref = FusekiBuilder.buildDataAccessPoint(subject, registry) ;
action.getDataAccessPointRegistry().register(datasetPath, ref) ;
action.getResponse().setContentType(WebContent.contentTypeTextPlain);
ServletOutputStream out = action.getResponse().getOutputStream() ;
ServletOps.success(action) ;
system.commit();
committed = true ;
} catch (IOException ex) { IO.exception(ex); }
finally {
if ( ! committed ) {
if ( systemFileCopy != null ) FileOps.deleteSilent(systemFileCopy);
if ( configFile != null ) FileOps.deleteSilent(configFile);
system.abort() ;
}
system.end() ;
}
return null ;
}
@Override
protected JsonValue execPostItem(HttpAction action) {
String name = action.getDatasetName() ;
if ( name == null )
name = "''" ;
action.log.info(format("[%d] POST dataset %s", action.id, name)) ;
if ( action.getDataAccessPoint() == null )
ServletOps.errorNotFound("Not found: dataset "+action.getDatasetName());
DataService dSrv = action.getDataService() ;
if ( dSrv == null )
// If not set explicitly, take from DataAccessPoint
dSrv = action.getDataAccessPoint().getDataService() ;
String s = action.request.getParameter("state") ;
if ( s == null || s.isEmpty() )
ServletOps.errorBadRequest("No state change given") ;
// setDatasetState is a transaction on the persistent state of the server.
if ( s.equalsIgnoreCase("active") ) {
action.log.info(format("[%d] REBUILD DATASET %s", action.id, name)) ;
setDatasetState(name, FusekiVocab.stateActive) ;
dSrv.goActive() ;
// DatasetGraph dsg = ???? ;
//dSrv.activate(dsg) ;
//dSrv.activate() ;
} else if ( s.equalsIgnoreCase("offline") ) {
action.log.info(format("[%d] OFFLINE DATASET %s", action.id, name)) ;
DataAccessPoint access = action.getDataAccessPoint() ;
//access.goOffline() ;
dSrv.goOffline() ; // Affects the target of the name.
setDatasetState(name, FusekiVocab.stateOffline) ;
//dSrv.offline() ;
} else if ( s.equalsIgnoreCase("unlink") ) {
action.log.info(format("[%d] UNLINK ACCESS NAME %s", action.id, name)) ;
DataAccessPoint access = action.getDataAccessPoint() ;
ServletOps.errorNotImplemented("unlink: dataset"+action.getDatasetName());
//access.goOffline() ;
// Registry?
}
else
ServletOps.errorBadRequest("State change operation '"+s+"' not recognized");
return null ;
}
private void assemblerFromBody(HttpAction action, StreamRDF dest) {
bodyAsGraph(action, dest) ;
}
private void assemblerFromForm(HttpAction action, StreamRDF dest) {
String dbType = action.getRequest().getParameter(paramDatasetType) ;
String dbName = action.getRequest().getParameter(paramDatasetName) ;
if ( StringUtils.isBlank(dbType) || StringUtils.isBlank(dbName) )
ServletOps.errorBadRequest("Required parameters: dbName and dbType");
Map<String, String> params = new HashMap<>() ;
if ( dbName.startsWith("/") )
params.put(Template.NAME, dbName.substring(1)) ;
else
params.put(Template.NAME, dbName) ;
FusekiServer.addGlobals(params);
//action.log.info(format("[%d] Create database : name = %s, type = %s", action.id, dbName, dbType )) ;
if ( ! dbType.equals(tDatabasetTDB) && ! dbType.equals(tDatabasetMem) )
ServletOps.errorBadRequest(format("dbType can be only '%s' or '%s'", tDatabasetTDB, tDatabasetMem)) ;
String template = null ;
if ( dbType.equalsIgnoreCase(tDatabasetTDB))
template = TemplateFunctions.templateFile(Template.templateTDBFN, params, Lang.TTL) ;
if ( dbType.equalsIgnoreCase(tDatabasetMem))
template = TemplateFunctions.templateFile(Template.templateMemFN, params, Lang.TTL) ;
RDFDataMgr.parse(dest, new StringReader(template), "http://base/", Lang.TTL) ;
}
private void assemblerFromUpload(HttpAction action, StreamRDF dest) {
Upload.fileUploadWorker(action, dest);
}
// ---- DELETE
@Override
protected void execDeleteItem(HttpAction action) {
// if ( isContainerAction(action) ) {
// ServletOps.errorBadRequest("DELETE only applies to a specific dataset.") ;
// return ;
// }
// Does not exist?
String name = action.getDatasetName() ;
if ( name == null )
name = "" ;
action.log.info(format("[%d] DELETE ds=%s", action.id, name)) ;
if ( ! action.getDataAccessPointRegistry().isRegistered(name) )
ServletOps.errorNotFound("No such dataset registered: "+name);
systemDSG.begin(ReadWrite.WRITE) ;
boolean committed = false ;
try {
// Here, go offline.
// Need to reference count operations when they drop to zero
// or a timer goes off, we delete the dataset.
DataAccessPoint ref = action.getDataAccessPointRegistry().get(name) ;
// Redo check inside transaction.
if ( ref == null )
ServletOps.errorNotFound("No such dataset registered: "+name);
// Make it invisible to the outside.
action.getDataAccessPointRegistry().remove(name) ;
// Delete configuration file.
// Should be only one, undo damage if multiple.
FusekiEnv.existingConfigurationFile(name).stream().forEach(FileOps::deleteSilent);
// Find graph associated with this dataset name.
// (Statically configured databases aren't in the system database.)
Node n = NodeFactory.createLiteral(DataAccessPoint.canonical(name)) ;
Quad q = getOne(systemDSG, null, null, pServiceName.asNode(), n) ;
// if ( q == null )
// ServletOps.errorBadRequest("Failed to find dataset for '"+name+"'");
if ( q != null ) {
Node gn = q.getGraph() ;
//action.log.info("SHUTDOWN NEEDED"); // To ensure it goes away?
systemDSG.deleteAny(gn, null, null, null) ;
}
systemDSG.commit() ;
committed = true ;
ServletOps.success(action) ;
} finally {
if ( ! committed ) systemDSG.abort() ;
systemDSG.end() ;
}
// Remove the configuration file (if any).
action.getDataAccessPointRegistry().remove(name) ;
}
// Persistent state change.
private void setDatasetState(String name, Resource newState) {
boolean committed = false ;
system.begin(ReadWrite.WRITE) ;
try {
String dbName = name ;
if ( dbName.startsWith("/") )
dbName = dbName.substring(1) ;
String update = StrUtils.strjoinNL
(SystemState.PREFIXES,
"DELETE { GRAPH ?g { ?s fu:status ?state } }",
"INSERT { GRAPH ?g { ?s fu:status "+FmtUtils.stringForRDFNode(newState)+" } }",
"WHERE {",
" GRAPH ?g { ?s fu:name '"+dbName+"' ; ",
" fu:status ?state .",
" }",
"}"
) ;
UpdateRequest req = UpdateFactory.create(update) ;
UpdateAction.execute(req, system);
system.commit();
committed = true ;
} finally {
if ( ! committed ) system.abort() ;
system.end() ;
}
}
// ---- Auxiliary functions
private static Quad getOne(DatasetGraph dsg, Node g, Node s, Node p, Node o) {
Iterator<Quad> iter = dsg.findNG(g, s, p, o) ;
if ( ! iter.hasNext() )
return null ;
Quad q = iter.next() ;
if ( iter.hasNext() )
return null ;
return q ;
}
private static Statement getOne(Model m, Resource s, Property p, RDFNode o) {
StmtIterator iter = m.listStatements(s, p, o) ;
if ( ! iter.hasNext() )
return null ;
Statement stmt = iter.next() ;
if ( iter.hasNext() )
return null ;
return stmt ;
}
// XXX Merge with Upload.incomingData
private static void bodyAsGraph(HttpAction action, StreamRDF dest) {
HttpServletRequest request = action.request ;
String base = ActionLib.wholeRequestURL(request) ;
ContentType ct = FusekiLib.getContentType(request) ;
Lang lang = RDFLanguages.contentTypeToLang(ct.getContentType()) ;
if ( lang == null ) {
ServletOps.errorBadRequest("Unknown content type for triples: " + ct) ;
return ;
}
InputStream input = null ;
try { input = request.getInputStream() ; }
catch (IOException ex) { IO.exception(ex) ; }
int len = request.getContentLength() ;
// if ( verbose ) {
// if ( len >= 0 )
// alog.info(format("[%d] Body: Content-Length=%d, Content-Type=%s, Charset=%s => %s", action.id, len,
// ct.getContentType(), ct.getCharset(), lang.getName())) ;
// else
// alog.info(format("[%d] Body: Content-Type=%s, Charset=%s => %s", action.id, ct.getContentType(),
// ct.getCharset(), lang.getName())) ;
// }
dest.prefix("root", base+"#");
ActionSPARQL.parse(action, dest, input, lang, base) ;
}
}