/*
* 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.jackrabbit.vfs.ext.ds;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Enumeration;
import java.util.Properties;
import javax.jcr.RepositoryException;
import org.apache.commons.vfs2.FileName;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileSystemManager;
import org.apache.commons.vfs2.FileSystemOptions;
import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
import org.apache.commons.vfs2.impl.StandardFileSystemManager;
import org.apache.commons.vfs2.util.DelegatingFileSystemOptionsBuilder;
import org.apache.jackrabbit.core.data.Backend;
import org.apache.jackrabbit.core.data.CachingDataStore;
import org.apache.jackrabbit.core.data.DataStoreException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Commons VFS based data store.
*/
public class VFSDataStore extends CachingDataStore {
static final String BASE_FOLDER_URI = "baseFolderUri";
static final String ASYNC_WRITE_POOL_SIZE = "asyncWritePoolSize";
static final String FILE_SYSTEM_MANAGER_CLASS_NAME = "fileSystemManagerClassName";
/**
* Logger instance.
*/
private static final Logger LOG = LoggerFactory.getLogger(VFSDataStore.class);
/**
* Property key name for maximum backend connection. e.g. max total http connections.
*/
static final String PROP_MAX_TOTAL_CONNECTIONS = "maxTotalConnections";
/**
* Property key name for maximum backend connection. e.g. max http connections per host.
*/
static final String PROP_MAX_CONNECTIONS_PER_HOST = "maxConnectionsPerHost";
/**
* Default maximum backend connection. e.g. max http connection.
*/
static final int DEFAULT_MAX_CONNECTION = 200;
/**
* Property key prefix for FielSystemOptions.
*/
private static final String FILE_SYSTEM_OPTIONS_PROP_PREFIX = "fso.";
/**
* The class name of the VFS {@link FileSystemManager} instance used in this VFS data store.
* If this is not set, then {@link StandardFileSystemManager} is used by default.
*/
private String fileSystemManagerClassName;
/**
* The VFS {@link FileSystemManager} instance used in this VFS data store.
* If {@link #fileSystemManagerClassName} is not set, then a {@link StandardFileSystemManager} instance is created by default.
*/
private FileSystemManager fileSystemManager;
/**
* {@link FileSystemOptions} used when resolving the {@link #baseFolder}.
*/
private FileSystemOptions fileSystemOptions;
/**
* Properties used when building a {@link FileSystemOptions} using {@link DelegatingFileSystemOptionsBuilder}.
*/
private Properties fileSystemOptionsProperties;
/**
* The VFS base folder URI where all the entry files are maintained.
*/
private String baseFolderUri;
/**
* The VFS base folder object where all the entry files are maintained.
*/
private FileObject baseFolder;
/**
* The pool size of asynchronous write pooling executor.
*/
private int asyncWritePoolSize = VFSBackend.DEFAULT_ASYNC_WRITE_POOL_SIZE;
@Override
public void init(String homeDir) throws RepositoryException {
overridePropertiesFromConfig();
if (baseFolderUri == null) {
throw new RepositoryException("VFS base folder URI must be set.");
}
fileSystemManager = createFileSystemManager();
FileName baseFolderName = null;
try {
baseFolderName = fileSystemManager.resolveURI(baseFolderUri);
FileSystemOptions fso = getFileSystemOptions();
if (fso != null) {
baseFolder = fileSystemManager.resolveFile(baseFolderUri, fso);
} else {
baseFolder = fileSystemManager.resolveFile(baseFolderUri);
}
baseFolder.createFolder();
} catch (FileSystemException e) {
throw new RepositoryException("Could not initialize the VFS base folder at '"
+ (baseFolderName == null ? "" : baseFolderName.getFriendlyURI()) + "'.", e);
}
super.init(homeDir);
}
@Override
public void close() throws DataStoreException {
VFSBackend backend = (VFSBackend) getBackend();
try {
// Let's wait for 5 minutes at max if there are still execution jobs in the async writing executor's queue.
int seconds = 0;
while (backend.getAsyncWriteExecutorActiveCount() > 0 && seconds++ < 300) {
Thread.sleep(1000);
}
} catch (InterruptedException e) {
LOG.warn("Interrupted while waiting for the async write executor to complete.", e);
}
// Commenting out the following because the javadoc of FileSystemManager#closeFileSystem(FileSystem)
// says it is dangerous when singleton instance is being used, which is the case as VFSDataStore keeps
// single file system manager instance.
// Also, VFS seems to remove the related provider component on that invocation.
// if (fileSystemManager != null) {
// fileSystemManager.closeFileSystem(baseFolder.getFileSystem());
// }
super.close();
}
/**
* {@inheritDoc}
*/
@Override
protected Backend createBackend() {
VFSBackend backend = new VFSBackend(baseFolder);
backend.setAsyncWritePoolSize(getAsyncWritePoolSize());
return backend;
}
/**
* {@inheritDoc}
*/
@Override
protected String getMarkerFile() {
return "vfs.init.done";
}
/**
* Returns the class name of the VFS {@link FileSystemManager} instance used in this VFS data store.
* @return
*/
public String getFileSystemManagerClassName() {
return fileSystemManagerClassName;
}
/**
* Sets the class name of the VFS {@link FileSystemManager} instance used in this VFS data store.
* If this is not set, then {@link StandardFileSystemManager} is used by default.
* @param fileSystemManagerClassName
*/
public void setFileSystemManagerClassName(String fileSystemManagerClassName) {
this.fileSystemManagerClassName = fileSystemManagerClassName;
}
/**
* Returns the VFS {@link FileSystemManager} instance used in this VFS data store.
* @return the VFS {@link FileSystemManager} instance used in this VFS data store
*/
public FileSystemManager getFileSystemManager() {
return fileSystemManager;
}
/**
* Returns {@link FileSystemOptions} instance used when resolving the {@link #baseFolder}.
* This may return null if {@link FileSystemOptions} instance was not injected or
* a {@link #fileSystemOptionsProperties} instance cannot be injected or created.
* Therefore, the caller should check whether or not this returns null.
* When returning null, the caller may not use a {@link FileSystemOptions} instance.
* @return {@link FileSystemOptions} instance used when resolving the {@link #baseFolder}
*/
public FileSystemOptions getFileSystemOptions() throws RepositoryException {
if (fileSystemOptions == null) {
fileSystemOptions = createFileSystemOptions();
}
return fileSystemOptions;
}
/**
* Sets the {@link FileSystemOptions} instance used when resolving the {@link #baseFolder}.
* @param fileSystemOptions {@link FileSystemOptions} instance used when resolving the {@link #baseFolder}
*/
public void setFileSystemOptions(FileSystemOptions fileSystemOptions) {
this.fileSystemOptions = fileSystemOptions;
}
/**
* Sets the properties used when building a {@link FileSystemOptions}, using {@link DelegatingFileSystemOptionsBuilder}.
* @param fileSystemOptionsProperties properties used when building a {@link FileSystemOptions}
*/
public void setFileSystemOptionsProperties(Properties fileSystemOptionsProperties) {
this.fileSystemOptionsProperties = fileSystemOptionsProperties;
}
/**
* Sets the properties in a semi-colon delimited string used when building a {@link FileSystemOptions},
* using {@link DelegatingFileSystemOptionsBuilder}.
* @param fileSystemOptionsPropertiesInString properties in String
*/
public void setFileSystemOptionsPropertiesInString(String fileSystemOptionsPropertiesInString) {
if (fileSystemOptionsPropertiesInString != null) {
try {
StringReader reader = new StringReader(fileSystemOptionsPropertiesInString);
Properties props = new Properties();
props.load(reader);
fileSystemOptionsProperties = props;
} catch (IOException e) {
throw new IllegalArgumentException("Could not load file system options properties.", e);
}
}
}
/**
* Sets the base VFS folder URI.
* @param baseFolderUri base VFS folder URI
*/
public void setBaseFolderUri(String baseFolderUri) {
this.baseFolderUri = baseFolderUri;
}
/**
* Returns the pool size of the async write pool executor.
* @return the pool size of the async write pool executor
*/
public int getAsyncWritePoolSize() {
return asyncWritePoolSize;
}
/**
* Sets the pool size of the async write pool executor.
* @param asyncWritePoolSize pool size of the async write pool executor
*/
public void setAsyncWritePoolSize(int asyncWritePoolSize) {
this.asyncWritePoolSize = asyncWritePoolSize;
}
/**
* Creates a {@link FileSystemManager} instance.
* @return a {@link FileSystemManager} instance.
* @throws RepositoryException if an error occurs creating the manager.
*/
protected FileSystemManager createFileSystemManager() throws RepositoryException {
FileSystemManager fileSystemManager = null;
try {
if (getFileSystemManagerClassName() == null) {
fileSystemManager = new StandardFileSystemManager();
} else {
final Class<?> mgrClass = Class.forName(getFileSystemManagerClassName());
fileSystemManager = (FileSystemManager) mgrClass.newInstance();
}
if (fileSystemManager instanceof DefaultFileSystemManager) {
((DefaultFileSystemManager) fileSystemManager).init();
}
} catch (final FileSystemException e) {
throw new RepositoryException(
"Could not initialize file system manager of class: " + getFileSystemManagerClassName(), e);
} catch (final Exception e) {
throw new RepositoryException(
"Could not create file system manager of class: " + getFileSystemManagerClassName(), e);
}
return fileSystemManager;
}
/**
* Builds and returns {@link FileSystemOptions} instance which is used when resolving the {@link #baseFolder}
* during the initialization.
* If {@link #fileSystemOptionsProperties} is available, this scans all the property key names starting with {@link #FILE_SYSTEM_OPTIONS_PROP_PREFIX}
* and uses the rest of the key name after the {@link #FILE_SYSTEM_OPTIONS_PROP_PREFIX} as the combination of scheme and property name
* when building a {@link FileSystemOptions} using {@link DelegatingFileSystemOptionsBuilder}.
* @return {@link FileSystemOptions} instance which is used when resolving the {@link #baseFolder} during the initialization
* @throws RepositoryException if any file system exception occurs
*/
protected FileSystemOptions createFileSystemOptions() throws RepositoryException {
FileSystemOptions fso = null;
if (fileSystemOptionsProperties != null) {
try {
fso = new FileSystemOptions();
DelegatingFileSystemOptionsBuilder delegate = new DelegatingFileSystemOptionsBuilder(getFileSystemManager());
String key;
String schemeDotPropName;
String scheme;
String propName;
String value;
int offset;
for (Enumeration<?> e = fileSystemOptionsProperties.propertyNames(); e.hasMoreElements(); ) {
key = (String) e.nextElement();
if (key.startsWith(FILE_SYSTEM_OPTIONS_PROP_PREFIX)) {
value = fileSystemOptionsProperties.getProperty(key);
schemeDotPropName = key.substring(FILE_SYSTEM_OPTIONS_PROP_PREFIX.length());
offset = schemeDotPropName.indexOf('.');
if (offset > 0) {
scheme = schemeDotPropName.substring(0, offset);
propName = schemeDotPropName.substring(offset + 1);
delegate.setConfigString(fso, scheme, propName, value);
} else {
LOG.warn("Ignoring an FileSystemOptions property in invalid format. Key: {}, Value: {}", key, value);
}
}
}
} catch (FileSystemException e) {
throw new RepositoryException("Could not create File System Options.", e);
}
}
return fso;
}
/**
* Returns properties used when building a {@link FileSystemOptions} instance by the properties
* during the initialization.
* @return properties used when building a {@link FileSystemOptions} instance by the properties during the initialization
*/
protected Properties getFileSystemOptionsProperties() {
return fileSystemOptionsProperties;
}
private void overridePropertiesFromConfig() throws RepositoryException {
final String config = getConfig();
// If config param provided, then override properties from the config file.
if (config != null && !"".equals(config)) {
try {
final Properties props = readConfig(config);
String propValue = props.getProperty(ASYNC_WRITE_POOL_SIZE);
if (propValue != null && !"".equals(propValue)) {
setAsyncWritePoolSize(Integer.parseInt(propValue));
}
propValue = props.getProperty(BASE_FOLDER_URI);
if (propValue != null && !"".equals(propValue)) {
setBaseFolderUri(propValue);
}
propValue = props.getProperty(FILE_SYSTEM_MANAGER_CLASS_NAME);
if (propValue != null && !"".equals(propValue)) {
setFileSystemManagerClassName(propValue);
}
final Properties fsoProps = new Properties();
String propName;
for (Enumeration<?> propNames = props.propertyNames(); propNames.hasMoreElements(); ) {
propName = (String) propNames.nextElement();
if (propName.startsWith(FILE_SYSTEM_OPTIONS_PROP_PREFIX)) {
fsoProps.setProperty(propName, props.getProperty(propName));
}
}
if (!fsoProps.isEmpty()) {
this.setFileSystemOptionsProperties(fsoProps);
}
} catch (IOException e) {
throw new RepositoryException("Configuration file doesn't exist at '" + config + "'.");
}
}
}
private Properties readConfig(String fileName) throws IOException {
if (!new File(fileName).exists()) {
throw new IOException("Config file not found: " + fileName);
}
Properties prop = new Properties();
InputStream in = null;
try {
in = new FileInputStream(fileName);
prop.load(in);
} finally {
if (in != null) {
in.close();
}
}
return prop;
}
}