All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.felix.cm.file.FilePersistenceManager Maven / Gradle / Ivy

There is a newer version: 1.9.26
Show newest version
/*
 * 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:
PIDConfiguration File Name
samplesample.config
org.apache.felix.log.LogServiceorg/apache/felix/log/LogService.config
sample.flächesample/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() { @Override public Object run() { _delete( pid ); return null; } }, acc ); } private void _delete( final String pid ) { synchronized ( this ) { getFile( pid ).delete(); } } /** * Returns true if a (configuration) file exists for the given * identifier. * * @param pid The identifier of the configuration file to check. * * @return true 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() { @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 * Dictionary object. * * @param pid The identifier of the configuration file to delete. * * @return The configuration read from the file. This Dictionary * 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() { @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 cfgFile into a new * Dictionary object. * * @param cfgFile * The file from which to load the data. * @return A new Dictionary 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 Dictionary 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() { @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 pid encoding it as * follows: *
    *
  • Dots (.) are replaced by File.separatorChar *
  • Characters not matching [a-zA-Z0-9 _-] are encoded with a percent * character (%) and a 4-place hexadecimal unicode value. *
* 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 DictionaryEnumeration class implements the * Enumeration returning configuration Dictionary * objects on behalf of the {@link FilePersistenceManager#getDictionaries()} * method. *

* This enumeration loads configuration lazily with a look ahead of one * dictionary. */ @SuppressWarnings("rawtypes") class DictionaryEnumeration implements Enumeration { private Stack 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() { @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; } } }