blob: f9c0c120361da5a71773a074218e5a09bd716290 [file] [log] [blame]
Felix Meschbergeradd2b4a2007-04-11 18:12:33 +00001/*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19package org.apache.felix.cm.file;
20
21
22import java.io.File;
23import java.io.FileInputStream;
24import java.io.FileOutputStream;
25import java.io.IOException;
26import java.io.InputStream;
27import java.io.OutputStream;
28import java.util.BitSet;
29import java.util.Dictionary;
30import java.util.Enumeration;
31import java.util.Hashtable;
32import java.util.NoSuchElementException;
33import java.util.Stack;
34
35import org.apache.felix.cm.PersistenceManager;
36import org.osgi.framework.BundleContext;
37import org.osgi.framework.Constants;
38
39
40/**
41 * The <code>FilePersistenceManager</code> class stores configuration data in
42 * properties-like files inside a given directory. All configuration files are
43 * located in the same directory.
44 * <p>
45 * The configuration directory may be set by using the
46 * {@link #FilePersistenceManager(String)} naming the path to the directry. When
47 * this persistence manager is used by the Configuration Admin Service, the
48 * location may be configured using the {@link #CM_CONFIG_DIR} bundle context
49 * property.
50 * <p>
51 * If the location is not set, the <code>config</code> directory in the current
52 * working directory (as set in the <code>user.dir</code> system property) is
53 * used. If the the location is set but, no such directory exists, the directory
54 * and any missing parent directories are created. If a file exists at the given
55 * location, the constructor fails.
56 * <p>
57 * Configuration files are created in the configuration directory by appending
58 * the extension <code>.config</code> to the PID of the configuration. The PID
59 * is converted into a relative path name by replacing enclosed dots to slashes.
60 * Non-<code>symbolic-name</code> characters in the PID are encoded with their
61 * Unicode character code in hexadecimal.
62 * <p>
63 * <table border="0" cellspacing="3" cellpadding="0">
64 * <tr><td colspan="2"><b>Examples of PID to name conversion:</td></tr>
65 * <tr><th>PID</th><th>Configuration File Name</th></tr>
66 * <tr><td><code>sample</code><td><code>sample.config</code></tr>
67 * <tr><td><code>org.apache.felix.log.LogService</code><td><code>org/apache/felix/log/LogService.config</code></tr>
Felix Meschbergere2ca8332007-06-06 06:27:31 +000068 * <tr><td><code>sample.fl�che</code><td><code>sample/fl%00e8che.config</code></tr>
Felix Meschbergeradd2b4a2007-04-11 18:12:33 +000069 * </table>
70 *
71 * @author fmeschbe
72 */
73public class FilePersistenceManager implements PersistenceManager
74{
75
76 /**
77 * The extension of the configuration files.
78 */
79 private static final String FILE_EXT = ".config";
80
81 private static final BitSet VALID_PATH_CHARS;
82
83 /**
84 * The abstract path name of the configuration files.
85 */
86 private final File location;
87
88 // sets up this class defining the set of valid characters in path
89 // set getFile(String) for details.
90 static
91 {
92 VALID_PATH_CHARS = new BitSet();
93
94 for ( int i = 'a'; i <= 'z'; i++ )
95 {
96 VALID_PATH_CHARS.set( i );
97 }
98 for ( int i = 'A'; i <= 'Z'; i++ )
99 {
100 VALID_PATH_CHARS.set( i );
101 }
102 for ( int i = '0'; i <= '9'; i++ )
103 {
104 VALID_PATH_CHARS.set( i );
105 }
106 VALID_PATH_CHARS.set( File.separatorChar );
107 VALID_PATH_CHARS.set( ' ' );
108 VALID_PATH_CHARS.set( '-' );
109 VALID_PATH_CHARS.set( '_' );
110 }
111
112
113 /**
114 * Creates an instance of this persistence manager using the given location
115 * as the directory to store and retrieve the configuration files.
116 *
117 * @param location The configuration file location. If this is
118 * <code>null</code> the <code>config</code> directory below the current
119 * working directory is used.
120 *
121 * @throws IllegalArgumentException If the location exists but is not a
122 * directory or does not exist and cannot be created.
123 */
124 public FilePersistenceManager( String location )
125 {
126 if ( location == null )
127 {
128 location = System.getProperty( "user.dir" ) + "/config";
129 }
130
131 // check the location
132 File locationFile = new File( location );
133 if ( !locationFile.isDirectory() )
134 {
135 if ( locationFile.exists() )
136 {
137 throw new IllegalArgumentException( location + " is not a directory" );
138 }
139
140 if ( !locationFile.mkdirs() )
141 {
142 throw new IllegalArgumentException( "Cannot create directory " + location );
143 }
144 }
145
146 this.location = locationFile;
147 }
148
149
150 /**
151 * Loads configuration data from the configuration location and returns
152 * it as <code>Dictionary</code> objects.
153 * <p>
154 * This method is a lazy implementation, which is just one configuration
155 * file ahead of the current enumeration location.
156 *
157 * @return an enumeration of configuration data returned as instances of
158 * the <code>Dictionary</code> class.
159 */
160 public Enumeration getDictionaries()
161 {
162 return new DictionaryEnumeration();
163 }
164
165
166 /**
167 * Deletes the file for the given identifier.
168 *
169 * @param pid The identifier of the configuration file to delete.
170 */
171 public void delete( String pid )
172 {
173 getFile( pid ).delete();
174 }
175
176
177 /**
178 * Returns <code>true</code> if a (configuration) file exists for the given
179 * identifier.
180 *
181 * @param pid The identifier of the configuration file to check.
182 *
183 * @return <code>true</code> if the file exists
184 */
185 public boolean exists( String pid )
186 {
187 return getFile( pid ).isFile();
188 }
189
190
191 /**
192 * Reads the (configuration) for the given identifier into a
193 * <code>Dictionary</code> object.
194 *
195 * @param pid The identifier of the configuration file to delete.
196 *
197 * @return The configuration read from the file. This <code>Dictionary</code>
198 * may be empty if the file contains no configuration information
199 * or is not properly formatted.
200 */
201 public Dictionary load( String pid ) throws IOException
202 {
203 return load( getFile( pid ) );
204 }
205
206
207 /**
208 * Stores the contents of the <code>Dictionary</code> in a file denoted
209 * by the given identifier.
210 *
211 * @param pid The identifier of the configuration file to which to write
212 * the configuration contents.
213 * @param props The configuration data to write.
214 *
215 * @throws IOException If an error occurrs writing the configuration data.
216 */
217 public void store( String pid, Dictionary props ) throws IOException
218 {
219 OutputStream out = null;
220 try
221 {
222 File cfgFile = getFile( pid );
223
224 // ensure parent path
225 cfgFile.getParentFile().mkdirs();
226
227
228 out = new FileOutputStream( cfgFile );
229 ConfigurationHandler.write( out, props );
230 }
231 finally
232 {
233 if ( out != null )
234 {
235 try
236 {
237 out.close();
238 }
239 catch ( IOException ioe )
240 {
241 // ignore
242 }
243 }
244 }
245 }
246
247
248 /**
249 * Loads the contents of the <code>cfgFile</code> into a new
250 * <code>Dictionary</code> object.
251 *
252 * @param cfgFile The file from which to load the data.
253 *
254 * @return A new <code>Dictionary</code> object providing the file contents.
255 *
256 * @throws java.io.FileNotFoundException If the given file does not exist.
257 * @throws IOException If an error occurrs reading the configuration file.
258 */
259 private Dictionary load( File cfgFile ) throws IOException
260 {
261 InputStream ins = null;
262 try
263 {
264 ins = new FileInputStream( cfgFile );
265 return ConfigurationHandler.read( ins );
266 }
267 finally
268 {
269 if ( ins != null )
270 {
271 try
272 {
273 ins.close();
274 }
275 catch ( IOException ioe )
276 {
277 // ignore
278 }
279 }
280 }
281 }
282
283
284 /**
285 * Creates an abstract path name for the <code>pid</code> encoding it as
286 * follows:
287 * <ul>
288 * <li>Dots (<code>.</code>) are replaced by <code>File.separatorChar</code>
289 * <li>Characters not matching [a-zA-Z0-9 _-] are encoded with a percent
290 * character (<code>%</code>) and a 4-place hexadecimal unicode value.
291 * </ul>
292 * Before returning the path name, the parent directory and any ancestors
293 * are created.
294 *
295 * @param pid The identifier for which to create the abstract file name.
296 *
297 * @return The abstract path name, which the parent directory path created.
298 */
299 private File getFile( String pid )
300 {
301 // replace dots by File.separatorChar
302 pid = pid.replace( '.', File.separatorChar );
303
304 // replace slash by File.separatorChar if different
305 if ( File.separatorChar != '/' )
306 {
307 pid = pid.replace( '/', File.separatorChar );
308 }
309
310 // scan for first non-valid character (if any)
311 int first = 0;
312 while ( first < pid.length() && VALID_PATH_CHARS.get( pid.charAt( first ) ) )
313 {
314 first++;
315 }
316
317 // check whether we exhausted
318 if ( first < pid.length() )
319 {
320 StringBuffer buf = new StringBuffer( pid.substring( 0, first ) );
321
322 for ( int i = first; i < pid.length(); i++ )
323 {
324 char c = pid.charAt( i );
325 if ( VALID_PATH_CHARS.get( c ) )
326 {
327 buf.append( c );
328 }
329 else
330 {
331 String val = "000" + Integer.toHexString( c );
332 buf.append( '%' );
333 buf.append( val.substring( val.length() - 4 ) );
334 }
335 }
336
337 pid = buf.toString();
338 }
339
340 return new File( location, pid + FILE_EXT );
341 }
342
343 /**
344 * The <code>DictionaryEnumeration</code> class implements the
345 * <code>Enumeration</code> returning configuration <code>Dictionary</code>
346 * objects on behalf of the {@link FilePersistenceManager#getDictionaries()}
347 * method.
348 * <p>
349 * This enumeration loads configuration lazily with a look ahead of one
350 * dictionary.
351 *
352 * @author fmeschbe
353 */
354 private class DictionaryEnumeration implements Enumeration
355 {
356 private Stack dirStack;
357 private File[] fileList;
358 private int idx;
359 private Dictionary next;
360
361
362 DictionaryEnumeration()
363 {
364 dirStack = new Stack();
365 fileList = null;
366 idx = 0;
367
368 dirStack.push( location );
369 next = seek();
370 }
371
372
373 public boolean hasMoreElements()
374 {
375 return next != null;
376 }
377
378
379 public Object nextElement()
380 {
381 if ( next == null )
382 {
383 throw new NoSuchElementException();
384 }
385
386 Dictionary toReturn = next;
387 next = seek();
388 return toReturn;
389 }
390
391
392 private Dictionary seek()
393 {
394 while ( ( fileList != null && idx < fileList.length ) || !dirStack.isEmpty() )
395 {
396 if ( fileList == null || idx >= fileList.length )
397 {
398 File dir = ( File ) dirStack.pop();
399 fileList = dir.listFiles();
400 idx = 0;
401 }
402 else
403 {
404
405 File cfgFile = fileList[idx++];
406 if ( cfgFile.isFile() )
407 {
408 try
409 {
410 Dictionary dict = load( cfgFile );
411
Felix Meschbergere2ca8332007-06-06 06:27:31 +0000412 // use the dictionary if it has no PID or the PID
Felix Meschbergeradd2b4a2007-04-11 18:12:33 +0000413 // derived file name matches the source file name
Felix Meschbergere2ca8332007-06-06 06:27:31 +0000414 if ( dict.get( Constants.SERVICE_PID ) == null
415 || cfgFile.equals( getFile( ( String ) dict.get( Constants.SERVICE_PID ) ) ) )
Felix Meschbergeradd2b4a2007-04-11 18:12:33 +0000416 {
417 return dict;
418 }
419 }
420 catch ( IOException ioe )
421 {
422 // ignore, check next file
423 }
424 }
425 else if ( cfgFile.isDirectory() )
426 {
427 dirStack.push( cfgFile );
428 }
429 }
430 }
431
432 // exhausted
433 return null;
434 }
435 }
436}