FELIX-11 Implement Configuration Admin (Initial Checkin)
git-svn-id: https://svn.apache.org/repos/asf/incubator/felix/trunk@527592 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/configadmin/src/main/java/org/apache/felix/cm/file/FilePersistenceManager.java b/configadmin/src/main/java/org/apache/felix/cm/file/FilePersistenceManager.java
new file mode 100644
index 0000000..8b99b0b
--- /dev/null
+++ b/configadmin/src/main/java/org/apache/felix/cm/file/FilePersistenceManager.java
@@ -0,0 +1,436 @@
+/*
+ * 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.util.BitSet;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.NoSuchElementException;
+import java.util.Stack;
+
+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 may be set by using the
+ * {@link #FilePersistenceManager(String)} naming the path to the directry. When
+ * this persistence manager is used by the Configuration Admin Service, the
+ * location may be configured using the {@link #CM_CONFIG_DIR} 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.
+ * <p>
+ * <table border="0" cellspacing="3" cellpadding="0">
+ * <tr><td colspan="2"><b>Examples of PID to name conversion:</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èche</code><td><code>sample/fl%00e8che.config</code></tr>
+ * </table>
+ *
+ * @author fmeschbe
+ */
+public class FilePersistenceManager implements PersistenceManager
+{
+
+ /**
+ * The extension of the configuration files.
+ */
+ private static final String FILE_EXT = ".config";
+
+ private static final BitSet VALID_PATH_CHARS;
+
+ /**
+ * The abstract path name of the configuration files.
+ */
+ private final File location;
+
+ // 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( '_' );
+ }
+
+
+ /**
+ * Creates an instance of this persistence manager using the given location
+ * as the directory to store and retrieve the configuration files.
+ *
+ * @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.
+ */
+ public FilePersistenceManager( String location )
+ {
+ if ( location == null )
+ {
+ location = System.getProperty( "user.dir" ) + "/config";
+ }
+
+ // check the location
+ File locationFile = new File( 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;
+ }
+
+
+ /**
+ * 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.
+ */
+ public Enumeration getDictionaries()
+ {
+ return new DictionaryEnumeration();
+ }
+
+
+ /**
+ * Deletes the file for the given identifier.
+ *
+ * @param pid The identifier of the configuration file to delete.
+ */
+ public void delete( String pid )
+ {
+ 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
+ */
+ public boolean exists( String pid )
+ {
+ 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.
+ */
+ public Dictionary load( String pid ) throws IOException
+ {
+ return load( getFile( pid ) );
+ }
+
+
+ /**
+ * 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.
+ */
+ public void store( String pid, Dictionary props ) throws IOException
+ {
+ OutputStream out = null;
+ try
+ {
+ File cfgFile = getFile( pid );
+
+ // ensure parent path
+ cfgFile.getParentFile().mkdirs();
+
+
+ out = new FileOutputStream( cfgFile );
+ ConfigurationHandler.write( out, props );
+ }
+ finally
+ {
+ if ( out != null )
+ {
+ try
+ {
+ out.close();
+ }
+ catch ( IOException ioe )
+ {
+ // ignore
+ }
+ }
+ }
+ }
+
+
+ /**
+ * 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.
+ */
+ private Dictionary load( File cfgFile ) throws IOException
+ {
+ InputStream ins = null;
+ try
+ {
+ ins = new FileInputStream( cfgFile );
+ return ConfigurationHandler.read( ins );
+ }
+ finally
+ {
+ if ( ins != null )
+ {
+ try
+ {
+ ins.close();
+ }
+ catch ( IOException ioe )
+ {
+ // ignore
+ }
+ }
+ }
+ }
+
+
+ /**
+ * 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.
+ */
+ private File getFile( 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() )
+ {
+ StringBuffer buf = new StringBuffer( 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
+ {
+ String val = "000" + Integer.toHexString( c );
+ buf.append( '%' );
+ buf.append( val.substring( val.length() - 4 ) );
+ }
+ }
+
+ pid = buf.toString();
+ }
+
+ return new File( location, 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.
+ *
+ * @author fmeschbe
+ */
+ private 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( location );
+ next = seek();
+ }
+
+
+ public boolean hasMoreElements()
+ {
+ return next != null;
+ }
+
+
+ public Object nextElement()
+ {
+ if ( next == null )
+ {
+ throw new NoSuchElementException();
+ }
+
+ Dictionary toReturn = next;
+ next = seek();
+ return toReturn;
+ }
+
+
+ private Dictionary seek()
+ {
+ while ( ( fileList != null && idx < fileList.length ) || !dirStack.isEmpty() )
+ {
+ if ( fileList == null || idx >= fileList.length )
+ {
+ File dir = ( File ) dirStack.pop();
+ fileList = dir.listFiles();
+ idx = 0;
+ }
+ else
+ {
+
+ File cfgFile = fileList[idx++];
+ if ( cfgFile.isFile() )
+ {
+ try
+ {
+ Dictionary dict = load( cfgFile );
+
+ // use the dictionary if it has a PID and 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;
+ }
+ }
+}