org.apache.felix.cm.file.FilePersistenceManager Maven / Gradle / Ivy
Show all versions of org.apache.felix.configadmin Show documentation
/*
* 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.felix.cm.file;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.BitSet;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.StringTokenizer;
import org.apache.felix.cm.PersistenceManager;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
/**
* The FilePersistenceManager
class stores configuration data in
* properties-like files inside a given directory. All configuration files are
* located in the same directory.
*
* The configuration directory is set by either the
* {@link #FilePersistenceManager(String)} constructor or the
* {@link #FilePersistenceManager(BundleContext, String)} constructor. Refer
* to the respective JavaDocs for more information.
*
* When this persistence manager is used by the Configuration Admin Service,
* the location may be configured using the
* {@link org.apache.felix.cm.impl.ConfigurationManager#CM_CONFIG_DIR} bundle
* context property. That is the Configuration Admin Service creates an instance
* of this class calling
* new FilePersistenceManager(bundleContext, bundleContext.getProperty(CM_CONFIG_DIR))
.
*
* If the location is not set, the config
directory in the current
* working directory (as set in the user.dir
system property) is
* used. If the the location is set but, no such directory exists, the directory
* and any missing parent directories are created. If a file exists at the given
* location, the constructor fails.
*
* Configuration files are created in the configuration directory by appending
* the extension .config
to the PID of the configuration. The PID
* is converted into a relative path name by replacing enclosed dots to slashes.
* Non-symbolic-name
characters in the PID are encoded with their
* Unicode character code in hexadecimal.
*
*
* Examples of PID to name conversion:
* PID Configuration File Name
* sample
sample.config
* org.apache.felix.log.LogService
org/apache/felix/log/LogService.config
* sample.fläche
sample/fl%00e8che.config
*
*
* Mulithreading Issues
*
* In a multithreaded environment the {@link #store(String, Dictionary)} and
* {@link #load(String)} methods may be called at the the quasi-same time for the
* same configuration PID. It may no happen, that the store method starts
* writing the file and the load method might at the same time read from the
* file currently being written and thus loading corrupt data (if data is
* available at all).
*
* To prevent this situation from happening, the methods use synchronization
* and temporary files as follows:
*
* - The {@link #store(String, Dictionary)} method writes a temporary file
* with file extension
.tmp
. When done, the file is renamed to
* actual configuration file name as implied by the PID. This last step of
* renaming the file is synchronized on the FilePersistenceManager instance.
* - The {@link #load(String)} method is completeley synchronized on the
* FilePersistenceManager instance such that the {@link #store} method might
* inadvertantly try to replace the file while it is being read.
* - Finally the
Iterator
returned by {@link #getDictionaries()}
* is implemented such that any temporary configuration file is just ignored.
*
*/
public class FilePersistenceManager implements PersistenceManager
{
/**
* The default configuration data directory if no location is configured
* (value is "config").
*/
public static final String DEFAULT_CONFIG_DIR = "config";
/**
* The name of this persistence manager when registered in the service registry.
* (value is "file").
*/
public static final String DEFAULT_PERSISTENCE_MANAGER_NAME = "file";
/**
* The extension of the configuration files.
*/
private static final String FILE_EXT = ".config";
/**
* The extension of the configuration files, while they are being written
* (value is ".tmp").
*
* @see #store(String, Dictionary)
*/
private static final String TMP_EXT = ".tmp";
private static final BitSet VALID_PATH_CHARS;
/**
* The access control context we use in the presence of a security manager.
*/
private final AccessControlContext acc;
/**
* The abstract path name of the configuration files.
*/
private final File location;
/**
* Flag indicating whether this instance is running on a Windows
* platform or not.
*/
private final boolean isWin;
// sets up this class defining the set of valid characters in path
// set getFile(String) for details.
static
{
VALID_PATH_CHARS = new BitSet();
for ( int i = 'a'; i <= 'z'; i++ )
{
VALID_PATH_CHARS.set( i );
}
for ( int i = 'A'; i <= 'Z'; i++ )
{
VALID_PATH_CHARS.set( i );
}
for ( int i = '0'; i <= '9'; i++ )
{
VALID_PATH_CHARS.set( i );
}
VALID_PATH_CHARS.set( File.separatorChar );
VALID_PATH_CHARS.set( ' ' );
VALID_PATH_CHARS.set( '-' );
VALID_PATH_CHARS.set( '_' );
}
{
// Windows Specific Preparation (FELIX-4302)
// According to the GetJavaProperties method in
// jdk/src/windows/native/java/lang/java_props_md.c the os.name
// system property on all Windows platforms start with the string
// "Windows" hence we assume a Windows platform in thus case.
final String osName = System.getProperty( "os.name" );
isWin = osName != null && osName.startsWith( "Windows" );
}
private static boolean equalsNameWithPrefixPlusOneDigit( String name, String prefix) {
if ( name.length() != prefix.length() + 1 ) {
return false;
}
if ( !name.startsWith(prefix) ) {
return false;
}
char charAfterPrefix = name.charAt( prefix.length() );
return charAfterPrefix > '0' && charAfterPrefix < '9';
}
private static boolean isWinReservedName(String name) {
String upperCaseName = name.toUpperCase();
if ( "CON".equals( upperCaseName ) ) {
return true;
} else if ( "PRN".equals( upperCaseName ) ){
return true;
} else if ( "AUX".equals( upperCaseName ) ){
return true;
} else if ( "CLOCK$".equals( upperCaseName ) ){
return true;
} else if ( "NUL".equals( upperCaseName ) ){
return true;
} else if ( equalsNameWithPrefixPlusOneDigit( upperCaseName, "COM") ) {
return true;
} else if ( equalsNameWithPrefixPlusOneDigit( upperCaseName, "LPT") ){
return true;
}
return false;
}
/**
* Creates an instance of this persistence manager using the given location
* as the directory to store and retrieve the configuration files.
*
* This constructor resolves the configuration file location as follows:
*
* - If
location
is null
, the config
* directory in the current working directory as specified in the
* user.dir
system property is assumed.
* - Otherwise the named directory is used.
* - If the directory name resolved in the first or second step is not an
* absolute path, it is resolved to an absolute path calling the
*
File.getAbsoluteFile()
method.
* - If a non-directory file exists as the location found in the previous
* step or the named directory (including any parent directories) cannot be
* created, an
IllegalArgumentException
is thrown.
*
*
* This constructor is equivalent to calling
* {@link #FilePersistenceManager(BundleContext, String)} with a
* null
BundleContext
.
*
* @param location The configuration file location. If this is
* null
the config
directory below the current
* working directory is used.
*
* @throws IllegalArgumentException If the location
exists but
* is not a directory or does not exist and cannot be created.
*/
public FilePersistenceManager( String location )
{
this( null, location );
}
/**
* Creates an instance of this persistence manager using the given location
* as the directory to store and retrieve the configuration files.
*
* This constructor resolves the configuration file location as follows:
*
* - If
location
is null
, the config
* directory in the persistent storage area of the bundle identified by
* bundleContext
is used.
* - If the framework does not support persistent storage area for bundles
* in the filesystem or if
bundleContext
is null
,
* the config
directory in the current working directory as
* specified in the user.dir
system property is assumed.
* - Otherwise the named directory is used.
* - If the directory name resolved in the first, second or third step is
* not an absolute path and a
bundleContext
is provided which
* provides access to persistent storage area, the directory name is
* resolved as being inside the persistent storage area. Otherwise the
* directory name is resolved to an absolute path calling the
* File.getAbsoluteFile()
method.
* - If a non-directory file exists as the location found in the previous
* step or the named directory (including any parent directories) cannot be
* created, an
IllegalArgumentException
is thrown.
*
*
* @param bundleContext The BundleContext
to optionally get
* the data location for the configuration files. This may be
* null
, in which case this constructor acts exactly the
* same as calling {@link #FilePersistenceManager(String)}.
* @param location The configuration file location. If this is
* null
the config
directory below the current
* working directory is used.
*
* @throws IllegalArgumentException If the location exists but is not a
* directory or does not exist and cannot be created.
* @throws IllegalStateException If the bundleContext
is not
* valid.
*/
public FilePersistenceManager( BundleContext bundleContext, String location )
{
// setup the access control context from the calling setup
if ( System.getSecurityManager() != null )
{
acc = AccessController.getContext();
}
else
{
acc = null;
}
// no configured location, use the config dir in the bundle persistent
// area
if ( location == null && bundleContext != null )
{
File locationFile = bundleContext.getDataFile( DEFAULT_CONFIG_DIR );
if ( locationFile != null )
{
location = locationFile.getAbsolutePath();
}
}
// fall back to the current working directory if the platform does
// not support filesystem based data area
if ( location == null )
{
location = System.getProperty( "user.dir" ) + "/config";
}
// ensure the file is absolute
File locationFile = new File( location );
if ( !locationFile.isAbsolute() )
{
if ( bundleContext != null )
{
File bundleLocationFile = bundleContext.getDataFile( locationFile.getPath() );
if ( bundleLocationFile != null )
{
locationFile = bundleLocationFile;
}
}
// ensure the file object is an absolute file object
locationFile = locationFile.getAbsoluteFile();
}
// check the location
if ( !locationFile.isDirectory() )
{
if ( locationFile.exists() )
{
throw new IllegalArgumentException( location + " is not a directory" );
}
if ( !locationFile.mkdirs() )
{
throw new IllegalArgumentException( "Cannot create directory " + location );
}
}
this.location = locationFile;
}
/**
* Encodes a Service PID to a filesystem path as described in the class
* JavaDoc above.
*
* This method is not part of the API of this class and is declared package
* private to enable JUnit testing on it. This method may be removed or
* modified at any time without notice.
*
* @param pid The Service PID to encode into a relative path name.
*
* @return The relative path name corresponding to the Service PID.
*/
String encodePid( String pid )
{
// replace dots by File.separatorChar
pid = pid.replace( '.', File.separatorChar );
// replace slash by File.separatorChar if different
if ( File.separatorChar != '/' )
{
pid = pid.replace( '/', File.separatorChar );
}
// scan for first non-valid character (if any)
int first = 0;
while ( first < pid.length() && VALID_PATH_CHARS.get( pid.charAt( first ) ) )
{
first++;
}
// check whether we exhausted
if ( first < pid.length() )
{
StringBuilder buf = new StringBuilder( pid.substring( 0, first ) );
for ( int i = first; i < pid.length(); i++ )
{
char c = pid.charAt( i );
if ( VALID_PATH_CHARS.get( c ) )
{
buf.append( c );
}
else
{
appendEncoded( buf, c );
}
}
pid = buf.toString();
}
// Prefix special device names on Windows (FELIX-4302)
if ( isWin )
{
final StringTokenizer segments = new StringTokenizer( pid, File.separator, true );
final StringBuilder pidBuffer = new StringBuilder( pid.length() );
while ( segments.hasMoreTokens() )
{
final String segment = segments.nextToken();
if ( isWinReservedName(segment) )
{
appendEncoded( pidBuffer, segment.charAt( 0 ) );
pidBuffer.append( segment.substring( 1 ) );
}
else
{
pidBuffer.append( segment );
}
}
pid = pidBuffer.toString();
}
return pid;
}
private void appendEncoded( StringBuilder buf, final char c )
{
String val = "000" + Integer.toHexString( c );
buf.append( '%' );
buf.append( val.substring( val.length() - 4 ) );
}
/**
* Returns the directory in which the configuration files are written as
* a File
object.
*
* @return The configuration file location.
*/
public File getLocation()
{
return location;
}
/**
* Loads configuration data from the configuration location and returns
* it as Dictionary
objects.
*
* This method is a lazy implementation, which is just one configuration
* file ahead of the current enumeration location.
*
* @return an enumeration of configuration data returned as instances of
* the Dictionary
class.
*/
@SuppressWarnings("rawtypes")
@Override
public Enumeration getDictionaries()
{
return new DictionaryEnumeration();
}
/**
* Deletes the file for the given identifier.
*
* @param pid The identifier of the configuration file to delete.
*/
@Override
public void delete( final String pid )
{
if ( System.getSecurityManager() != null )
{
_privilegedDelete( pid );
}
else
{
_delete( pid );
}
}
private void _privilegedDelete( final String pid )
{
AccessController.doPrivileged( new PrivilegedAction