/**
* 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.server;
import java.io.File ;
import java.io.IOException ;
import java.io.InputStream ;
import java.io.StringReader ;
import java.net.URL ;
import java.nio.file.Files ;
import java.nio.file.Path ;
import java.nio.file.StandardCopyOption ;
import java.util.* ;
import jena.cmd.CmdException ;
import org.apache.jena.atlas.io.IO ;
import org.apache.jena.atlas.lib.DS ;
import org.apache.jena.atlas.lib.FileOps ;
import org.apache.jena.atlas.lib.InternalErrorException ;
import org.apache.jena.fuseki.Fuseki ;
import org.apache.jena.fuseki.FusekiConfigException ;
import org.apache.jena.fuseki.build.* ;
import org.apache.jena.fuseki.servlets.ServletOps ;
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.sparql.core.DatasetGraph ;
import org.apache.jena.tdb.sys.Names ;
public class FusekiServer
{
// Initialization of FUSEKI_HOME and FUSEKI_BASE is done in FusekiEnv.setEnvironment()
// so that the code is independent of any logging. FusekiLogging can use
// initialized values of FUSEKI_BASE while looking forlog4j configuration.
/** Root of the Fuseki installation for fixed files.
* This may be null (e.g. running inside a web application container) */
//public static Path FUSEKI_HOME = null ;
/** Root of the varying files in this deployment. Often $FUSEKI_HOME/run.
* This is not null - it may be /etc/fuseki, which must be writable.
*/
//public static Path FUSEKI_BASE = null ;
// Relative names of directories in the FUSEKI_BASE area.
public static final String runArea = FusekiEnv.ENV_runArea ;
public static final String databasesLocationBase = "databases" ;
// Place to put Lucene text and spatial indexes.
//private static final String databaseIndexesDir = "indexes" ;
public static final String backupDirNameBase = "backups" ;
public static final String configDirNameBase = "configuration" ;
public static final String logsNameBase = "logs" ;
public static final String systemDatabaseNameBase = "system" ;
public static final String systemFileAreaBase = "system_files" ;
public static final String templatesNameBase = "templates" ;
// This name is in web.xml as well.
public static final String DFT_SHIRO_INI = "shiro.ini" ;
// In FUSEKI_BASE
public static final String DFT_CONFIG = "config.ttl" ;
/** Directory for TDB databases - this is known to the assembler templates */
public static Path dirDatabases = null ;
/** Directory for writing backups */
public static Path dirBackups = null ;
/** Directory for assembler files */
public static Path dirConfiguration = null ;
/** Directory for assembler files */
public static Path dirLogs = null ;
/** Directory for system database */
public static Path dirSystemDatabase = null ;
/** Directory for files uploaded (e.g upload assmbler descriptions); not data uploads. */
public static Path dirFileArea = null ;
/** Directory for assembler files */
public static Path dirTemplates = null ;
private static boolean initialized = false ;
// Marks the end of successful initialization.
/*package*/static boolean serverInitialized = false ;
/*package*/ synchronized static void formatBaseArea() {
if ( initialized )
return ;
initialized = true ;
try {
FusekiEnv.setEnvironment() ;
Path FUSEKI_HOME = FusekiEnv.FUSEKI_HOME ;
Path FUSEKI_BASE = FusekiEnv.FUSEKI_BASE ;
Fuseki.init() ;
Fuseki.configLog.info("FUSEKI_HOME="+ ((FUSEKI_HOME==null) ? "unset" : FUSEKI_HOME.toString())) ;
Fuseki.configLog.info("FUSEKI_BASE="+FUSEKI_BASE.toString());
// ---- Check FUSEKI_HOME and FUSEKI_BASE
// If FUSEKI_HOME exists, it may be FUSEKI_BASE.
if ( FUSEKI_HOME != null ) {
if ( ! Files.isDirectory(FUSEKI_HOME) )
throw new FusekiConfigException("FUSEKI_HOME is not a directory: "+FUSEKI_HOME) ;
if ( ! Files.isReadable(FUSEKI_HOME) )
throw new FusekiConfigException("FUSEKI_HOME is not readable: "+FUSEKI_HOME) ;
}
if ( Files.exists(FUSEKI_BASE) ) {
if ( ! Files.isDirectory(FUSEKI_BASE) )
throw new FusekiConfigException("FUSEKI_BASE is not a directory: "+FUSEKI_BASE) ;
if ( ! Files.isWritable(FUSEKI_BASE) )
throw new FusekiConfigException("FUSEKI_BASE is not writable: "+FUSEKI_BASE) ;
} else {
ensureDir(FUSEKI_BASE);
}
// Ensure FUSEKI_BASE has the assumed directories.
dirTemplates = writeableDirectory(FUSEKI_BASE, templatesNameBase) ;
dirDatabases = writeableDirectory(FUSEKI_BASE, databasesLocationBase) ;
dirBackups = writeableDirectory(FUSEKI_BASE, backupDirNameBase) ;
dirConfiguration = writeableDirectory(FUSEKI_BASE, configDirNameBase) ;
dirLogs = writeableDirectory(FUSEKI_BASE, logsNameBase) ;
dirSystemDatabase = writeableDirectory(FUSEKI_BASE, systemDatabaseNameBase) ;
dirFileArea = writeableDirectory(FUSEKI_BASE, systemFileAreaBase) ;
//Possible intercept point
// ---- Initialize with files.
if ( Files.isRegularFile(FUSEKI_BASE) )
throw new FusekiConfigException("FUSEKI_BASE exists but is a file") ;
// Copy missing files into FUSEKI_BASE
copyFileIfMissing(null, DFT_SHIRO_INI, FUSEKI_BASE) ;
copyFileIfMissing(null, DFT_CONFIG, FUSEKI_BASE) ;
for ( String n : Template.templateNames ) {
copyFileIfMissing(null, n, FUSEKI_BASE) ;
}
serverInitialized = true ;
} catch (RuntimeException ex) {
Fuseki.serverLog.error("Exception in server initialization", ex) ;
throw ex ;
}
}
private static boolean emptyDir(Path dir) {
return dir.toFile().list().length <= 2 ;
}
/** Copy a file from src to dst under name fn.
* If src is null, try as a classpath resource
* @param src Source directory, or null meaning use java resource.
* @param fn File name, a relative path.
* @param dst Destination directory.
*
*/
private static void copyFileIfMissing(Path src, String fn, Path dst) {
Path dstFile = dst.resolve(fn) ;
if ( Files.exists(dstFile) )
return ;
// fn may be a path.
if ( src != null ) {
try {
Files.copy(src.resolve(fn), dstFile, StandardCopyOption.COPY_ATTRIBUTES) ;
} catch (IOException e) {
IO.exception("Failed to copy file "+src, e);
e.printStackTrace();
}
} else {
try {
// Get from the file from area "org/apache/jena/fuseki/server" (our package)
URL url = FusekiServer.class.getResource(fn) ;
if ( url == null )
throw new FusekiConfigException("Field to find resource '"+fn+"'") ;
InputStream in = url.openStream() ;
Files.copy(in, dstFile) ;
}
catch (IOException e) {
IO.exception("Failed to copy file from resource: "+src, e);
e.printStackTrace();
}
}
}
public static void initializeDataAccessPoints(DataAccessPointRegistry registry, ServerInitialConfig initialSetup, String configDir) {
List<DataAccessPoint> configFileDBs = initServerConfiguration(initialSetup) ;
List<DataAccessPoint> directoryDBs = FusekiConfig.readConfigurationDirectory(configDir) ;
List<DataAccessPoint> systemDBs = FusekiConfig.readSystemDatabase(SystemState.getDataset()) ;
List<DataAccessPoint> datapoints = new ArrayList<DataAccessPoint>() ;
datapoints.addAll(configFileDBs) ;
datapoints.addAll(directoryDBs) ;
datapoints.addAll(systemDBs) ;
// Having found them, set them all running.
enable(registry, datapoints);
}
private static void enable(DataAccessPointRegistry registry, List<DataAccessPoint> datapoints) {
for ( DataAccessPoint dap : datapoints ) {
Fuseki.configLog.info("Register: "+dap.getName()) ;
registry.register(dap.getName(), dap);
}
}
private static List<DataAccessPoint> initServerConfiguration(ServerInitialConfig params) {
// Has a side effect of global context setting
// when processing a config file.
// Compatibility.
List<DataAccessPoint> datasets = DS.list() ;
if ( params == null )
return datasets ;
if ( params.fusekiCmdLineConfigFile != null ) {
List<DataAccessPoint> confDatasets = processServerConfigFile(params.fusekiCmdLineConfigFile) ;
datasets.addAll(confDatasets) ;
}
else if ( params.fusekiServerConfigFile != null ) {
List<DataAccessPoint> confDatasets = processServerConfigFile(params.fusekiServerConfigFile) ;
datasets.addAll(confDatasets) ;
}
else if ( params.dsg != null ) {
DataAccessPoint dap = datasetDefaultConfiguration(params.datasetPath, params.dsg, params.allowUpdate) ;
datasets.add(dap) ;
} else if ( params.argTemplateFile != null ) {
DataAccessPoint dap = configFromTemplate(params.argTemplateFile, params.datasetPath, params.allowUpdate, params.params) ;
datasets.add(dap) ;
}
// No datasets is valid.
return datasets ;
}
private static List<DataAccessPoint> processServerConfigFile(String configFilename) {
if ( ! FileOps.exists(configFilename) ) {
Fuseki.configLog.warn("Configuration file '" + configFilename+"' does not exist") ;
return Collections.emptyList();
}
Fuseki.configLog.info("Configuration file: " + configFilename) ;
return FusekiConfig.readServerConfigFile(configFilename) ;
}
private static DataAccessPoint configFromTemplate(String templateFile, String datasetPath,
boolean allowUpdate, Map<String, String> params) {
DatasetDescriptionRegistry registry = FusekiServer.registryForBuild() ;
// ---- Setup
if ( params == null ) {
params = new HashMap<>() ;
params.put(Template.NAME, datasetPath) ;
} else {
if ( ! params.containsKey(Template.NAME) ) {
Fuseki.configLog.warn("No NAME found in template parameters (added)") ;
params.put(Template.NAME, datasetPath) ;
}
}
//-- Logging
Fuseki.configLog.info("Template file: " + templateFile) ;
String dir = params.get(Template.DIR) ;
if ( dir != null ) {
if ( Objects.equals(dir, Names.memName) ) {
Fuseki.configLog.info("TDB dataset: in-memory") ;
} else {
if ( !FileOps.exists(dir) )
throw new CmdException("Directory not found: " + dir) ;
Fuseki.configLog.info("TDB dataset: directory=" + dir) ;
}
}
//-- Logging
datasetPath = DataAccessPoint.canonical(datasetPath) ;
// DRY -- ActionDatasets (and others?)
addGlobals(params);
String str = TemplateFunctions.templateFile(templateFile, params, Lang.TTL) ;
Lang lang = RDFLanguages.filenameToLang(str, Lang.TTL) ;
StringReader sr = new StringReader(str) ;
Model model = ModelFactory.createDefaultModel() ;
RDFDataMgr.read(model, sr, datasetPath, lang);
// ---- DataAccessPoint
Statement stmt = getOne(model, null, FusekiVocab.pServiceName, null) ;
if ( stmt == null ) {
StmtIterator sIter = model.listStatements(null, FusekiVocab.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") ;
}
Resource subject = stmt.getSubject() ;
if ( ! allowUpdate ) {
// Opportunity for more sophisticated "read-only" mode.
// 1 - clean model, remove "fu:serviceUpdate", "fu:serviceUpload", "fu:serviceReadGraphStore", "fu:serviceReadWriteGraphStore"
// 2 - set a flag on DataAccessPoint
}
DataAccessPoint dap = FusekiBuilder.buildDataAccessPoint(subject, registry) ;
return dap ;
}
public static void addGlobals(Map<String, String> params) {
if ( params == null ) {
Fuseki.configLog.warn("FusekiServer.addGlobals : params is null", new Throwable()) ;
return ;
}
if ( ! params.containsKey("FUSEKI_BASE") )
params.put("FUSEKI_BASE", pathStringOrElse(FusekiEnv.FUSEKI_BASE, "unset")) ;
if ( ! params.containsKey("FUSEKI_HOME") )
params.put("FUSEKI_HOME", pathStringOrElse(FusekiEnv.FUSEKI_HOME, "unset")) ;
}
private static String pathStringOrElse(Path path, String dft) {
if ( path == null )
return dft ;
return path.toString() ;
}
// DRY -- ActionDatasets (and others?)
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 ;
}
private static DataAccessPoint datasetDefaultConfiguration( String name, DatasetGraph dsg, boolean allowUpdate) {
name = DataAccessPoint.canonical(name) ;
DataService ds = FusekiBuilder.buildDataService(dsg, allowUpdate) ;
DataAccessPoint dap = new DataAccessPoint(name, ds) ;
return dap ;
}
// ---- Helpers
/** Ensure a directory exists, creating it if necessary.
*/
private static void ensureDir(Path directory) {
File dir = directory.toFile() ;
if ( ! dir.exists() ) {
boolean b = dir.mkdirs() ;
if ( ! b )
throw new FusekiConfigException("Failed to create directory: "+directory) ;
}
else if ( ! dir.isDirectory())
throw new FusekiConfigException("Not a directory: "+directory) ;
}
private static void mustExist(Path directory) {
File dir = directory.toFile() ;
if ( ! dir.exists() )
throw new FusekiConfigException("Does not exist: "+directory) ;
if ( ! dir.isDirectory())
throw new FusekiConfigException("Not a directory: "+directory) ;
}
private static boolean exists(Path directory) {
File dir = directory.toFile() ;
return dir.exists() ;
}
private static Path writeableDirectory(Path root , String relName ) {
Path p = makePath(root, relName) ;
ensureDir(p);
if ( ! Files.isWritable(p) )
throw new FusekiConfigException("Not writable: "+p) ;
return p ;
}
private static Path makePath(Path root , String relName ) {
Path path = root.resolve(relName) ;
// Must exist
// try { path = path.toRealPath() ; }
// catch (IOException e) { IO.exception(e) ; }
return path ;
}
/**
* <ul>
* <li>GLOBAL: sharing across all descriptions
* <li>FILE: sharing within files but not across files.
* </ul>
*/
enum DatasetDescriptionScope { GLOBAL, FILE }
private static DatasetDescriptionRegistry globalDatasets = new DatasetDescriptionRegistry() ;
private static DatasetDescriptionScope policyDatasetDescriptionScope = DatasetDescriptionScope.FILE ;
/** Call this once per configuration file. */
public static DatasetDescriptionRegistry registryForBuild() {
switch (policyDatasetDescriptionScope) {
case FILE : return new DatasetDescriptionRegistry() ;
case GLOBAL : return globalDatasets ;
default: throw new InternalErrorException() ;
}
}
}