blob: 91c8419e46e5c1d97ba589840645846298ebf77e [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.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 <code>FilePersistenceManager</code> class stores configuration data in
* properties-like files inside a given directory. All configuration files are
* located in the same directory.
* <p>
* 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.
* <p>
* 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} bundle context
* property.
* <p>
* If the location is not set, the <code>config</code> directory in the current
* working directory (as set in the <code>user.dir</code> 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.
* <p>
* Configuration files are created in the configuration directory by appending
* the extension <code>.config</code> to the PID of the configuration. The PID
* is converted into a relative path name by replacing enclosed dots to slashes.
* Non-<code>symbolic-name</code> characters in the PID are encoded with their
* Unicode character code in hexadecimal.
*
* <table border="0" cellspacing="3" cellpadding="0" summary="Examples">
* <tr>
* <td colspan="2"><b>Examples of PID to name conversion:</b></td>
* </tr>
* <tr>
* <th>PID</th>
* <th>Configuration File Name</th>
* </tr>
* <tr>
* <td><code>sample</code>
* <td><code>sample.config</code>
* </tr>
* <tr>
* <td><code>org.apache.felix.log.LogService</code>
* <td><code>org/apache/felix/log/LogService.config</code>
* </tr>
* <tr>
* <td><code>sample.fl&auml;che</code>
* <td><code>sample/fl%00e8che.config</code>
* </tr>
* </table>
* <p>
* <b>Mulithreading Issues</b>
* <p>
* 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).
* <p>
* To prevent this situation from happening, the methods use synchronization and
* temporary files as follows:
* <ul>
* <li>The {@link #store(String, Dictionary)} method writes a temporary file
* with file extension <code>.tmp</code>. 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.</li>
* <li>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.</li>
* <li>Finally the <code>Iterator</code> returned by {@link #getDictionaries()}
* is implemented such that any temporary configuration file is just
* ignored.</li>
* </ul>
*/
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.
* <p>
* This constructor resolves the configuration file location as follows:
* <ul>
* <li>If <code>location</code> is <code>null</code>, the <code>config</code>
* directory in the current working directory as specified in the
* <code>user.dir</code> system property is assumed.</li>
* <li>Otherwise the named directory is used.</li>
* <li>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
* <code>File.getAbsoluteFile()</code> method.</li>
* <li>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 <code>IllegalArgumentException</code> is thrown.</li>
* </ul>
* <p>
* This constructor is equivalent to calling
* {@link #FilePersistenceManager(BundleContext, String)} with a
* <code>null</code> <code>BundleContext</code>.
*
* @param location The configuration file location. If this is
* <code>null</code> the <code>config</code> directory below the current
* working directory is used.
*
* @throws IllegalArgumentException If the <code>location</code> 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.
* <p>
* This constructor resolves the configuration file location as follows:
* <ul>
* <li>If <code>location</code> is <code>null</code>, the <code>config</code>
* directory in the persistent storage area of the bundle identified by
* <code>bundleContext</code> is used.</li>
* <li>If the framework does not support persistent storage area for bundles
* in the filesystem or if <code>bundleContext</code> is <code>null</code>,
* the <code>config</code> directory in the current working directory as
* specified in the <code>user.dir</code> system property is assumed.</li>
* <li>Otherwise the named directory is used.</li>
* <li>If the directory name resolved in the first, second or third step is
* not an absolute path and a <code>bundleContext</code> 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
* <code>File.getAbsoluteFile()</code> method.</li>
* <li>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 <code>IllegalArgumentException</code> is thrown.</li>
* </ul>
*
* @param bundleContext The <code>BundleContext</code> to optionally get
* the data location for the configuration files. This may be
* <code>null</code>, in which case this constructor acts exactly the
* same as calling {@link #FilePersistenceManager(String)}.
* @param location The configuration file location. If this is
* <code>null</code> the <code>config</code> 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 <code>bundleContext</code> 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.
* <p>
* 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 <code>File</code> object.
*
* @return The configuration file location.
*/
public File getLocation()
{
return location;
}
/**
* Loads configuration data from the configuration location and returns
* it as <code>Dictionary</code> objects.
* <p>
* 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 <code>Dictionary</code> 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<Object>()
{
@Override
public Object run()
{
_delete( pid );
return null;
}
}, acc );
}
private void _delete( final String pid )
{
synchronized ( this )
{
getFile( pid ).delete();
}
}
/**
* Returns <code>true</code> if a (configuration) file exists for the given
* identifier.
*
* @param pid The identifier of the configuration file to check.
*
* @return <code>true</code> if the file exists
*/
@Override
public boolean exists( final String pid )
{
if ( System.getSecurityManager() != null )
{
return _privilegedExists( pid );
}
return _exists( pid );
}
private boolean _privilegedExists( final String pid )
{
final Object result = AccessController.doPrivileged( new PrivilegedAction<Boolean>()
{
@Override
public Boolean run()
{
// FELIX-2771: Boolean.valueOf(boolean) is not in Foundation
return _exists( pid ) ? Boolean.TRUE : Boolean.FALSE;
}
} );
return ( ( Boolean ) result ).booleanValue();
}
private boolean _exists( final String pid )
{
synchronized ( this )
{
return getFile( pid ).isFile();
}
}
/**
* Reads the (configuration) for the given identifier into a
* <code>Dictionary</code> object.
*
* @param pid The identifier of the configuration file to delete.
*
* @return The configuration read from the file. This <code>Dictionary</code>
* may be empty if the file contains no configuration information
* or is not properly formatted.
*/
@SuppressWarnings("rawtypes")
@Override
public Dictionary load( String pid ) throws IOException
{
final File cfgFile = getFile( pid );
if ( System.getSecurityManager() != null )
{
return _privilegedLoad( cfgFile );
}
return _load( cfgFile );
}
@SuppressWarnings("rawtypes")
private Dictionary _privilegedLoad( final File cfgFile ) throws IOException
{
try
{
Dictionary result = AccessController.doPrivileged( new PrivilegedExceptionAction<Dictionary>()
{
@Override
public Dictionary run() throws IOException
{
return _load( cfgFile );
}
} );
return result;
}
catch ( PrivilegedActionException pae )
{
// FELIX-2771: getCause() is not available in Foundation
throw ( IOException ) pae.getException();
}
}
/**
* Loads the contents of the <code>cfgFile</code> into a new
* <code>Dictionary</code> object.
*
* @param cfgFile
* The file from which to load the data.
* @return A new <code>Dictionary</code> object providing the file contents.
* @throws java.io.FileNotFoundException
* If the given file does not exist.
* @throws IOException
* If an error occurrs reading the configuration file.
*/
@SuppressWarnings("rawtypes")
Dictionary _load( File cfgFile ) throws IOException
{
// this method is not part of the API of this class but is made
// package private to prevent the creation of a synthetic method
// for use by the DictionaryEnumeration._seek method
// synchronize this instance to make at least sure, the file is
// not at the same time accessed by another thread (see store())
// we have to synchronize the complete load time as the store
// method might want to replace the file while we are reading and
// still have the file open. This might be a problem e.g. in Windows
// environments, where files may not be removed which are still open
synchronized ( this )
{
InputStream ins = null;
try
{
ins = new FileInputStream( cfgFile );
return ConfigurationHandler.read( ins );
}
finally
{
if ( ins != null )
{
try
{
ins.close();
}
catch ( IOException ioe )
{
// ignore
}
}
}
}
}
/**
* Stores the contents of the <code>Dictionary</code> in a file denoted
* by the given identifier.
*
* @param pid The identifier of the configuration file to which to write
* the configuration contents.
* @param props The configuration data to write.
*
* @throws IOException If an error occurrs writing the configuration data.
*/
@SuppressWarnings("rawtypes")
@Override
public void store( final String pid, final Dictionary props ) throws IOException
{
if ( System.getSecurityManager() != null )
{
_privilegedStore( pid, props );
}
else
{
_store( pid, props );
}
}
@SuppressWarnings("rawtypes")
private void _privilegedStore( final String pid, final Dictionary props ) throws IOException
{
try
{
AccessController.doPrivileged( new PrivilegedExceptionAction<Object>()
{
@Override
public Object run() throws IOException
{
_store( pid, props );
return null;
}
} );
}
catch ( PrivilegedActionException pae )
{
// FELIX-2771: getCause() is not available in Foundation
throw ( IOException ) pae.getException();
}
}
@SuppressWarnings("rawtypes")
private void _store( final String pid, final Dictionary props ) throws IOException
{
OutputStream out = null;
File tmpFile = null;
try
{
File cfgFile = getFile( pid );
// ensure parent path
File cfgDir = cfgFile.getParentFile();
cfgDir.mkdirs();
// write the configuration to a temporary file
tmpFile = File.createTempFile( cfgFile.getName(), TMP_EXT, cfgDir );
out = new FileOutputStream( tmpFile );
ConfigurationHandler.write( out, props );
out.close();
// after writing the file, rename it but ensure, that no other
// might at the same time open the new file
// see load(File)
synchronized ( this )
{
// make sure the cfg file does not exists (just for sanity)
if ( cfgFile.exists() )
{
// FELIX-4165: detect failure to delete old file
if ( !cfgFile.delete() )
{
throw new IOException( "Cannot remove old file '" + cfgFile + "'; changes in '" + tmpFile
+ "' cannot be persisted at this time" );
}
}
// rename the temporary file to the new file
if ( !tmpFile.renameTo( cfgFile ) )
{
throw new IOException( "Failed to rename configuration file from '" + tmpFile + "' to '" + cfgFile );
}
}
}
finally
{
if ( out != null )
{
try
{
out.close();
}
catch ( IOException ioe )
{
// ignore
}
}
if (tmpFile != null && tmpFile.exists())
{
tmpFile.delete();
}
}
}
/**
* Creates an abstract path name for the <code>pid</code> encoding it as
* follows:
* <ul>
* <li>Dots (<code>.</code>) are replaced by <code>File.separatorChar</code>
* <li>Characters not matching [a-zA-Z0-9 _-] are encoded with a percent
* character (<code>%</code>) and a 4-place hexadecimal unicode value.
* </ul>
* Before returning the path name, the parent directory and any ancestors
* are created.
*
* @param pid The identifier for which to create the abstract file name.
*
* @return The abstract path name, which the parent directory path created.
*/
File getFile( String pid )
{
// this method is not part of the API of this class but is made
// package private to prevent the creation of a synthetic method
// for use by the DictionaryEnumeration._seek method
return new File( location, encodePid( pid ) + FILE_EXT );
}
/**
* The <code>DictionaryEnumeration</code> class implements the
* <code>Enumeration</code> returning configuration <code>Dictionary</code>
* objects on behalf of the {@link FilePersistenceManager#getDictionaries()}
* method.
* <p>
* This enumeration loads configuration lazily with a look ahead of one
* dictionary.
*/
@SuppressWarnings("rawtypes")
class DictionaryEnumeration implements Enumeration
{
private Stack<File> dirStack;
private File[] fileList;
private int idx;
private Dictionary next;
DictionaryEnumeration()
{
dirStack = new Stack<>();
fileList = null;
idx = 0;
dirStack.push( getLocation() );
next = seek();
}
@Override
public boolean hasMoreElements()
{
return next != null;
}
@Override
public Object nextElement()
{
if ( next == null )
{
throw new NoSuchElementException();
}
Dictionary toReturn = next;
next = seek();
return toReturn;
}
private Dictionary seek()
{
if ( System.getSecurityManager() != null )
{
return _privilegedSeek();
}
return _seek();
}
protected Dictionary _privilegedSeek()
{
Dictionary result = AccessController.doPrivileged( new PrivilegedAction<Dictionary>()
{
@Override
public Dictionary run()
{
return _seek();
}
} );
return result;
}
protected Dictionary _seek()
{
while ( ( fileList != null && idx < fileList.length ) || !dirStack.isEmpty() )
{
if ( fileList == null || idx >= fileList.length )
{
File dir = dirStack.pop();
fileList = dir.listFiles();
idx = 0;
}
else
{
File cfgFile = fileList[idx++];
if ( cfgFile.isFile() && !cfgFile.getName().endsWith( TMP_EXT ))
{
try
{
Dictionary dict = _load( cfgFile );
// use the dictionary if it has no PID or the PID
// derived file name matches the source file name
if ( dict.get( Constants.SERVICE_PID ) == null
|| cfgFile.equals( getFile( ( String ) dict.get( Constants.SERVICE_PID ) ) ) )
{
return dict;
}
}
catch ( IOException ioe )
{
// ignore, check next file
}
}
else if ( cfgFile.isDirectory() )
{
dirStack.push( cfgFile );
}
}
}
// exhausted
return null;
}
}
}