blob: efbcb5bdb42d3b0f7bb88b07e4c78de8390afb2f [file] [log] [blame]
/**
* 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 static org.apache.jena.fuseki.build.FusekiPrefixes.PREFIXES;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.apache.jena.atlas.RuntimeIOException;
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.logging.FmtLog;
import org.apache.jena.atlas.web.ContentType;
import org.apache.jena.datatypes.xsd.XSDDatatype;
import org.apache.jena.fuseki.build.DatasetDescriptionMap;
import org.apache.jena.fuseki.build.FusekiConfig;
import org.apache.jena.fuseki.ctl.ActionContainerItem;
import org.apache.jena.fuseki.ctl.JsonDescription;
import org.apache.jena.fuseki.server.DataAccessPoint;
import org.apache.jena.fuseki.server.DataService;
import org.apache.jena.fuseki.server.FusekiVocab;
import org.apache.jena.fuseki.server.ServerConst;
import org.apache.jena.fuseki.servlets.ActionLib;
import org.apache.jena.fuseki.servlets.HttpAction;
import org.apache.jena.fuseki.servlets.ServletOps;
import org.apache.jena.fuseki.system.FusekiNetLib;
import org.apache.jena.fuseki.system.Upload;
import org.apache.jena.fuseki.webapp.FusekiWebapp;
import org.apache.jena.fuseki.webapp.SystemState;
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.*;
import org.apache.jena.riot.system.StreamRDF;
import org.apache.jena.riot.system.StreamRDFLib;
import org.apache.jena.sparql.core.DatasetGraph;
import org.apache.jena.sparql.core.Quad;
import org.apache.jena.sparql.core.assembler.AssemblerUtils;
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.vocabulary.RDF;
import org.apache.jena.web.HttpSC;
public class ActionDatasets extends ActionContainerItem {
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 tDatabaseTDB = "tdb";
private static final String tDatabaseTDB2 = "tdb2";
private static final String tDatabaseMem = "mem";
public ActionDatasets() { super(); }
@Override
public void validate(HttpAction action) {}
// ---- 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(ServerConst.datasets);
JsonDescription.arrayDatasets(builder, action.getDataAccessPointRegistry());
builder.finishObject("D");
return builder.build();
}
@Override
protected JsonValue execGetItem(HttpAction action) {
String item = getItemDatasetName(action);
action.log.info(format("[%d] GET dataset %s", action.id, item));
JsonBuilder builder = new JsonBuilder();
DataAccessPoint dsDesc = getItemDataAccessPoint(action, item);
if ( dsDesc == null )
ServletOps.errorNotFound("Not found: dataset "+item);
JsonDescription.describe(builder, dsDesc);
return builder.build();
}
// ---- POST
@Override
protected JsonValue execPostContainer(HttpAction action) {
UUID uuid = UUID.randomUUID();
DatasetDescriptionMap registry = new DatasetDescriptionMap();
ContentType ct = ActionLib.getContentType(action);
boolean hasParams = action.request.getParameterNames().hasMoreElements();
if ( ct == null && ! hasParams )
ServletOps.errorBadRequest("Bad request - Content-Type or both parameters dbName and dbType required");
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 ( hasParams || WebContent.isHtmlForm(ct) )
assemblerFromForm(action, dest);
else if ( WebContent.isMultiPartForm(ct) )
assemblerFromUpload(action, dest);
else
assemblerFromBody(action, dest);
AssemblerUtils.addRegistered(model);
// ----
// Keep a persistent copy immediately. This is not used for
// anything other than being "for the record".
systemFileCopy = FusekiWebapp.dirFileArea.resolve(uuid.toString()).toString();
try ( OutputStream outCopy = IO.openOutputFile(systemFileCopy) ) {
RDFDataMgr.write(outCopy, model, Lang.TURTLE);
}
// ----
// Process configuration.
// Returns the "service fu:name NAME" statement
Statement stmt = findService(model);
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();
// This duplicates the code FusekiBuilder.buildDataAccessPoint to give better error messages and HTTP status code."
// ---- 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);
// ---- Check whether it already exists
if ( action.getDataAccessPointRegistry().isRegistered(datasetPath) )
// And abort.
ServletOps.error(HttpSC.CONFLICT_409, "Name already registered "+datasetPath);
}
action.log.info(format("[%d] Create database : name = %s", action.id, datasetPath));
configFile = FusekiWebapp.generateConfigurationFilename(datasetPath);
List<String> existing = FusekiWebapp.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 dataAccessPoint = FusekiConfig.buildDataAccessPoint(subject, registry);
dataAccessPoint.getDataService().setEndpointProcessors(action.getOperationRegistry());
dataAccessPoint.getDataService().goActive();
if ( ! datasetPath.equals(dataAccessPoint.getName()) )
FmtLog.warn(action.log, "Inconsistent names: datasetPath = %s; DataAccessPoint name = %s", datasetPath, dataAccessPoint);
action.getDataAccessPointRegistry().register(dataAccessPoint);
action.getResponse().setContentType(WebContent.contentTypeTextPlain);
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;
}
/** Find the service resource. There must be only one in the configuration. */
private Statement findService(Model model) {
// Try to find by unique pServiceName (max backwards compatibility)
// then try to find by rdf:type fuseki:Service.
// JENA-1794
Statement stmt = getOne(model, null, pServiceName, null);
// null means 0 or many, not one.
if ( stmt == null ) {
// This calculates { ?x rdf:type fu:Service ; ?x fu:name ?name }
// One and only one service.
Statement stmt2 = getOne(model, null, RDF.type, FusekiVocab.fusekiService);
if ( stmt2 == null ) {
int count = model.listStatements(null, RDF.type, FusekiVocab.fusekiService).toList().size();
if ( count == 0 )
ServletOps.errorBadRequest("No triple rdf:type fuseki:Service found");
else
ServletOps.errorBadRequest("Multiple Fuseki service descriptions");
}
Statement stmt3 = getOne(model, stmt2.getSubject(), pServiceName, null);
if ( stmt3 == null ) {
StmtIterator sIter = model.listStatements(stmt2.getSubject(), 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");
}
stmt = stmt3;
}
if ( ! stmt.getObject().isLiteral() )
ServletOps.errorBadRequest("Found "+FmtUtils.stringForRDFNode(stmt.getObject())+" : Service names are strings, then used to build the external URI");
return stmt;
}
@Override
protected JsonValue execPostItem(HttpAction action) {
String name = getItemDatasetName(action);
if ( name == null )
name = "''";
action.log.info(format("[%d] POST dataset %s", action.id, name));
// Not in the action - this not an ActionService.
DataAccessPoint dap = getItemDataAccessPoint(action, name);
if ( dap == null )
ServletOps.errorNotFound("Not found: dataset "+name);
DataService dSrv = dap.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"+name);
//access.goOffline();
// Registry?
}
else
ServletOps.errorBadRequest("State change operation '"+s+"' not recognized");
return null;
}
// ---- DELETE
@Override
protected void execDeleteItem(HttpAction action) {
// Does not exist?
String name = getItemDatasetName(action);
if ( name == null )
name = "";
action.log.info(format("[%d] DELETE dataset=%s", action.id, name));
if ( ! action.getDataAccessPointRegistry().isRegistered(name) )
ServletOps.errorNotFound("No such dataset registered: "+name);
// This acts as a lock.
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.
// Redo check inside transaction.
DataAccessPoint ref = action.getDataAccessPointRegistry().get(name);
if ( ref == null )
ServletOps.errorNotFound("No such dataset registered: "+name);
// Get a reference before removing.
DataService dataService = ref.getDataService();
// ---- Make it invisible in this running server.
action.getDataAccessPointRegistry().remove(name);
// Find the configuration.
String filename = name.startsWith("/") ? name.substring(1) : name;
List<String> configurationFiles = FusekiWebapp.existingConfigurationFile(filename);
if ( configurationFiles.isEmpty() ) {
// ---- Unmanaged
action.log.warn(format("[%d] Can't delete database configuration - not a managed database", action.id, name));
// ServletOps.errorOccurred(format("Can't delete database - not a managed configuration", name));
systemDSG.commit();
committed = true;
ServletOps.success(action);
return;
}
if ( configurationFiles.size() > 1 ) {
// -- This should not happen.
action.log.warn(format("[%d] There are %d configuration files, not one.", action.id, configurationFiles.size()));
ServletOps.errorOccurred(
format(
"There are %d configuration files, not one. Delete not performed; clearup of the filesystem needed.",
configurationFiles.size()));
return;
}
// ---- Remove managed database.
String cfgPathname = configurationFiles.get(0);
// Delete configuration file.
// Once deleted, server restart will not have the database.
FileOps.deleteSilent(cfgPathname);
// Delete the database for real only when it is in the server "run/databases"
// area. Don't delete databases that reside elsewhere. We do delete the
// configuration file, so the databases will not be associated with the server
// anymore.
boolean isTDB1 = org.apache.jena.tdb.sys.TDBInternal.isTDB1(dataService.getDataset());
boolean isTDB2 = org.apache.jena.tdb2.sys.TDBInternal.isTDB2(dataService.getDataset());
dataService.shutdown();
// JENA-1481: Really delete files.
if ( ( isTDB1 || isTDB2 ) ) {
// Delete databases created by the UI, or the admin operation, which are
// in predictable, unshared location on disk.
// There may not be any database files, the in-memory case.
Path pDatabase = FusekiWebapp.dirDatabases.resolve(filename);
if ( Files.exists(pDatabase)) {
try {
if ( Files.isSymbolicLink(pDatabase)) {
action.log.info(format("[%d] Database is a symbolic link, not removing files", action.id, pDatabase));
} else {
IO.deleteAll(pDatabase);
action.log.info(format("[%d] Deleted database files %s", action.id, pDatabase));
}
} catch (RuntimeIOException ex) {
action.log.error(format("[%d] Error while deleting database files %s: %s", action.id, pDatabase, ex.getMessage()), ex);
// But we have managed to remove it from the running server, and removed its configuration, so declare victory.
}
}
}
// -- System database
// 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();
}
}
private static void assemblerFromBody(HttpAction action, StreamRDF dest) {
bodyAsGraph(action, dest);
}
private static Map<String, String> dbTypeToTemplate = new HashMap<>();
static {
dbTypeToTemplate.put(tDatabaseTDB, Template.templateTDB1_FN);
dbTypeToTemplate.put(tDatabaseTDB2, Template.templateTDB2_FN);
dbTypeToTemplate.put(tDatabaseMem, Template.templateTIM_MemFN);
}
private static void assemblerFromForm(HttpAction action, StreamRDF dest) {
String x = action.getRequest().getQueryString();
String dbType = action.getRequest().getParameter(paramDatasetType);
String dbName = action.getRequest().getParameter(paramDatasetName);
if ( StringUtils.isBlank(dbType) || StringUtils.isBlank(dbName) )
ServletOps.errorBadRequest("Received HTML form. Both parameters 'dbName' and 'dbType' required");
Map<String, String> params = new HashMap<>();
if ( dbName.startsWith("/") )
params.put(Template.NAME, dbName.substring(1));
else
params.put(Template.NAME, dbName);
FusekiWebapp.addGlobals(params);
//action.log.info(format("[%d] Create database : name = %s, type = %s", action.id, dbName, dbType ));
String template = dbTypeToTemplate.get(dbType.toLowerCase(Locale.ROOT));
if ( template == null )
ServletOps.errorBadRequest(format("dbType can be only '%s', '%s' or '%s'", tDatabaseTDB, tDatabaseTDB2, tDatabaseMem));
String syntax = TemplateFunctions.templateFile(template, params, Lang.TTL);
RDFParser.create().source(new StringReader(syntax)).base("http://base/").lang(Lang.TTL).parse(dest);
}
private static void assemblerFromUpload(HttpAction action, StreamRDF dest) {
Upload.fileUploadWorker(action, dest);
}
// Persistent state change.
private static 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
(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;
}
// TODO Merge with Upload.incomingData
private static void bodyAsGraph(HttpAction action, StreamRDF dest) {
HttpServletRequest request = action.request;
String base = ActionLib.wholeRequestURL(request);
ContentType ct = FusekiNetLib.getContentType(request);
Lang lang = RDFLanguages.contentTypeToLang(ct.getContentTypeStr());
if ( lang == null ) {
ServletOps.errorBadRequest("Unknown content type for triples: " + ct);
return;
}
// Don't log - assemblers are typically small.
// Adding this to the log confuses things.
// Reserve logging for data uploads.
// long len = request.getContentLengthLong();
// if ( action.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+"#");
ActionLib.parseOrError(action, dest, lang, base);
}
}