/**
* Copyright 2007-2008 University Of Southern California
*
* Licensed 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 edu.isi.pegasus.common.util;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class creates a common interface to handle package properties.
* The package properties are meant as read-only (so far, until
* requirements crop up for write access). The class is implemented
* as a Singleton pattern.
*
* @author Jens-S. Vöckler
* @author Yong Zhao
* @author Karan Vahi
* @author Mats Rynge
* @version $Revision$
* */
public class CommonProperties implements Cloneable
{
/**
* implements the singleton access via class variable.
*/
private static CommonProperties m_instance = null;
/**
* internal set of properties. Direct access is expressly forbidden.
*/
private Properties m_props;
/**
* The bin dir of the Pegasus install
*/
private File m_binDir;
/**
* GNU: read-only single-machine data in DIR [PREFIX/etc].
* The files in this directory have a low change frequency, are
* effectively read-only, they reside on a per-machine basis, and they
* are usually valid for a single user.
*/
private File m_sysConfDir;
/**
* GNU: modifiable architecture-independent data in DIR [PREFIX/share/pegasus].
* The files in this directory have a high change frequency, are
* effectively read-write, can be shared via a networked FS, and they
* are usually valid for multiple users.
*/
private File m_sharedStateDir;
/**
* Location of our schemas
*/
private File m_schemaDir;
/**
* Basename of the file to read to obtain system properties
*/
public static final String PROPERTY_FILENAME = "properties";
/**
* Basename of the (new) file to read for user properties.
*/
public static final String USER_PROPERTY_FILENAME = ".pegasusrc";
/**
* Adds new properties to an existing set of properties while
* substituting variables. This function will allow value
* substitutions based on other property values. Value substitutions
* may not be nested. A value substitution will be ${property.key},
* where the dollar-brace and close-brace are being stripped before
* looking up the value to replace it with. Note that the ${..}
* combination must be escaped from the shell.
*
* @param a is the initial set of known properties (besides System ones)
* @param b is the set of properties to add to a
* @return the combined set of properties from a and b.
*/
protected static Properties addProperties( Properties a, Properties b )
{
// initial
Properties result = new Properties(a);
Properties sys = System.getProperties();
Pattern pattern = Pattern.compile( "\\$\\{[-a-zA-Z0-9._]+\\}" );
for ( Enumeration e = b.propertyNames(); e.hasMoreElements(); ) {
String key = (String) e.nextElement();
String value = b.getProperty(key);
// unparse value ${prop.key} inside braces
Matcher matcher = pattern.matcher(value);
StringBuffer sb = new StringBuffer();
while ( matcher.find() ) {
// extract name of properties from braces
String newKey = value.substring( matcher.start()+2, matcher.end()-1 );
// try to find a matching value in result properties
String newVal = result.getProperty(newKey);
/*
* // if not found, try b's properties
* if ( newVal == null ) newVal = b.getProperty(newKey);
*/
// try myself
if ( newVal == null ) newVal = result.getProperty(newKey);
// if still not found, try system properties
if ( newVal == null ) newVal = sys.getProperty(newKey);
// replace braced string with the actual value or empty string
matcher.appendReplacement( sb, newVal == null ? "" : newVal );
}
matcher.appendTail( sb );
result.setProperty( key, sb.toString() );
}
// final
return result;
}
/**
* Set some defaults, should values be missing in the dataset.
*
* @return the properties.
*/
private static Properties defaultProps()
{
// initial
Properties result = new Properties();
// copy pegasus keys as specified in the system properties to defaults
Properties sys = System.getProperties();
for ( Enumeration e = sys.propertyNames(); e.hasMoreElements(); ) {
String key = (String) e.nextElement();
if ( key.startsWith("pegasus.") )
result.setProperty( key, sys.getProperty(key) );
}
// INSERT HERE!
// final
return addProperties( new Properties(), result );
}
/**
* ctor. This initializes the local instance of properties
* from a central file.
*
* @param confProperties the path to conf properties, that supersede the loading
* of properties from $PEGASUS_HOME/.pegasusrc
*
* @exception IOException will be thrown if reading the property file
* goes awry.
* @exception MissingResourceException will be thrown if not all
* required properties are set
*/
protected CommonProperties( String confProperties )
throws IOException, MissingResourceException
{
// create empty new instance
this.m_props = new Properties( defaultProps() );
// first check for old -D option - this is just a warning
if ( System.getProperty("pegasus.properties") != null) {
File props = new File( System.getProperty("pegasus.properties") );
if( props.exists() ){
System.err.println( "[WARNING] Properties are no longer loaded from by" +
" -Dpegasus.properties property. " +
props.getAbsolutePath() + " will not be loaded. Use --conf option instead." );
}
}
// first check for old -D option - this is just a warning
if ( System.getProperty( "pegasus.user.properties" ) != null ) {
File props = new File(System.getProperty( "pegasus.user.properties" ));
if( props.exists() ){
System.err.println( "[WARNING] Properties are no longer loaded by " +
"specifying -Dpegasus.user.properties property. " +
props.getAbsolutePath() + " will not be loaded. Use --conf option instead." );
}
}
// add user properties afterwards to have higher precedence
String userHome = System.getProperty( "user.home", "." );
// try loading $HOME/.pegasusrc
File props = new File( userHome, CommonProperties.USER_PROPERTY_FILENAME );
//Prefer conf option over $HOME/.pegasusrc
File confProps = null;
props = ( confProperties != null &&
(confProps = new File( confProperties )).exists() )?
confProps :
props;
if ( props.exists() ) {
// if this file exists, read the properties (will throw IOException)
Properties temp = new Properties();
InputStream stream = new BufferedInputStream( new FileInputStream(props) );
temp.load( stream );
stream.close();
this.m_props = addProperties( this.m_props, temp );
}
// now set the paths: set sysconfdir to correct latest value
this.m_binDir = pickPath(
this.m_props.getProperty( "pegasus.home.bindir" ),
System.getProperty( "pegasus.home.bindir" ) );
this.m_sysConfDir = pickPath(
this.m_props.getProperty( "pegasus.home.sysconfdir" ),
System.getProperty( "pegasus.home.sysconfdir" ) );
this.m_sharedStateDir = pickPath(
this.m_props.getProperty( "pegasus.home.sharedstatedir" ),
System.getProperty( "pegasus.home.sharedstatedir" ) );
this.m_schemaDir = pickPath(
this.m_props.getProperty( "pegasus.home.schemadir" ),
System.getProperty( "pegasus.home.schemadir" ) );
}
private File pickPath(String p1, String p2) {
String winner = null;
if (p1 != null) {
winner = p1;
}
else if (p2 != null) {
winner = p2;
}
if (winner != null) {
return new File(winner);
}
return null;
}
/**
* Singleton threading: Creates the one and only instance of the
* properties in the current application.
*
* @return a reference to the properties.
* @exception IOException will be thrown if reading the property file
* goes awry.
* @exception MissingResourceException will be thrown if you forgot
* to specify the <code>-Dpegasus.home=$PEGASUS_HOME</code> to the runtime
* environment.
* @see #noHassleInstance()
*/
public static CommonProperties instance()
throws IOException, MissingResourceException
{
if ( CommonProperties.m_instance == null )
CommonProperties.m_instance =
new CommonProperties( null );
return CommonProperties.m_instance;
}
/**
* Create a temporary property that is not attached to the Singleton.
* This may be helpful with portal, which do magic things during the
* lifetime of a process.
*
*
* @param confProperties the path to conf properties, that supersede the
* loading of properties from $PEGASUS_HOME/.pegasusrc
*
* @return a reference to the parsed properties.
* @exception IOException will be thrown if reading the property file
* goes awry.
* @exception MissingResourceException will be thrown if you forgot
* to specify the <code>-Dpegasus.home=$PEGASUS_HOME</code> to the runtime
* environment.
* @see #instance()
*/
public static CommonProperties nonSingletonInstance( String confProperties )
throws IOException, MissingResourceException
{
return new CommonProperties( confProperties );
}
/**
* Singleton interface: Creates the one and only instance of the
* properties in the current application, and does not bother the
* programmer with exceptions. Rather, exceptions from the underlying
* <code>instance()</code> call are caught, converted to an error
* message on stderr, and the program is exited.
*
* @return a reference to the properties.
* @see #instance()
*/
public static CommonProperties noHassleInstance()
{
CommonProperties result = null;
try {
result = instance();
} catch ( IOException e ) {
System.err.println( "While reading property file: " + e.getMessage() );
System.exit(1);
} catch ( MissingResourceException mre ) {
System.err.println( mre.getMessage() );
System.exit(1);
}
return result;
}
/**
* Accessor pegasus bin directory
*
* @return the "bin" directory of the Pegasus runtime system.
*/
public File getBinDir()
{
return this.m_binDir;
}
/**
* Accessor to $PEGASUS_HOME/etc. The files in this directory have a low
* change frequency, are effectively read-only, they reside on a
* per-machine basis, and they are valid usually for a single user.
*
* @return the "etc" directory of the VDS runtime system.
*/
public File getSysConfDir()
{
return this.m_sysConfDir;
}
/**
* Accessor to $PEGASUS_HOME/com. The files in this directory have a high
* change frequency, are effectively read-write, they reside on a
* per-machine basis, and they are valid usually for a single user.
*
* @return the "com" directory of the VDS runtime system.
*/
public File getSharedStateDir()
{
return this.m_sharedStateDir;
}
/**
* Accessor to schema directory
*
* @return the schema directoru
*/
public File getSchemaDir()
{
return this.m_schemaDir;
}
/**
* Accessor: Obtains the number of properties known to the project.
*
* @return number of properties in the project property space.
*/
public int size()
{
return this.m_props.size();
}
/**
* Accessor: access to the internal properties as read from file.
* An existing system property of the same key will have precedence
* over any project property. This method will remove leading and
* trailing ASCII control characters and whitespaces.
*
* @param key is the key to look up
* @return the value for the key, or null, if not found.
*/
public String getProperty( String key )
{
String result =
System.getProperty( key, this.m_props.getProperty(key) );
return ( result == null ? result : result.trim() );
}
/**
* Accessor: access to the internal properties as read from file
* An existing system property of the same key will have precedence
* over any project property. This method will remove leading and
* trailing ASCII control characters and whitespaces.
*
* @param key is the key to look up
* @param defValue is a default to use, if no value can be found for the key.
* @return the value for the key, or the default value, if not found.
*/
public String getProperty( String key, String defValue )
{
String result =
System.getProperty( key, this.m_props.getProperty(key,defValue) );
return ( result == null ? result : result.trim() );
}
/**
* Accessor: Overwrite any properties from within the program.
*
* @param key is the key to look up
* @param value is the new property value to place in the system.
* @return the old value, or null if it didn't exist before.
*/
public Object setProperty( String key, String value )
{
//set in internal properties object also
//else prefix option does not work. Karan Oct 1, 2008
return this.m_props.setProperty( key, value );
//we don't set System properties, else it makes the clone method
//unusable
//return System.setProperty( key, value );
}
/**
* Accessor: enumerate all keys known to this property collection
* @return an enumerator for the keys of the properties.
*/
public Enumeration propertyNames()
{
return this.m_props.propertyNames();
}
/**
* Extracts a specific property key subset from the known properties.
* The prefix may be removed from the keys in the resulting dictionary,
* or it may be kept. In the latter case, exact matches on the prefix
* will also be copied into the resulting dictionary.
*
* @param prefix is the key prefix to filter the properties by.
* @param keepPrefix if true, the key prefix is kept in the resulting
* dictionary. As side-effect, a key that matches the prefix exactly
* will also be copied. If false, the resulting dictionary's keys are
* shortened by the prefix. An exact prefix match will not be copied,
* as it would result in an empty string key.
* @return a property dictionary matching the filter key. May be
* an empty dictionary, if no prefix matches were found.
*
* @see #getProperty( String ) is used to assemble matches
*/
public Properties matchingSubset( String prefix, boolean keepPrefix )
{
Properties result = new Properties();
// sanity check
if ( prefix == null || prefix.length() == 0 ) return result;
String prefixMatch; // match prefix strings with this
String prefixSelf; // match self with this
if ( prefix.charAt(prefix.length()-1) != '.' ) {
// prefix does not end in a dot
prefixSelf = prefix;
prefixMatch = prefix + '.';
} else {
// prefix does end in one dot, remove for exact matches
prefixSelf = prefix.substring( 0, prefix.length()-1 );
prefixMatch = prefix;
}
// POSTCONDITION: prefixMatch and prefixSelf are initialized!
// now add all matches into the resulting properties.
// Remark 1: #propertyNames() will contain the System properties!
// Remark 2: We need to give priority to System properties. This is done
// automatically by calling this class's getProperty method.
String key;
List<Enumeration> enumerations = new LinkedList();
//take cares of only the properties specified in the properties file
enumerations.add( propertyNames() );
//user may have specified profiles as properties in the system
//fix for PM-581
enumerations.add( System.getProperties().propertyNames() );
for( Enumeration e: enumerations ){
while( e.hasMoreElements() ){
key = (String) e.nextElement();
if ( keepPrefix ) {
// keep full prefix in result, also copy direct matches
if ( key.startsWith(prefixMatch) || key.equals(prefixSelf) )
result.setProperty( key,
getProperty(key) );
} else {
// remove full prefix in result, dont copy direct matches
if ( key.startsWith(prefixMatch) )
result.setProperty( key.substring( prefixMatch.length() ),
getProperty(key) );
}
}
}
// done
return result;
}
/**
* Extracts a specific property key subset from the properties passed.
* The prefix may be removed from the keys in the resulting dictionary,
* or it may be kept. In the latter case, exact matches on the prefix
* will also be copied into the resulting dictionary.
*
*
* @param prefix is the key prefix to filter the properties by.
* @param keepPrefix if true, the key prefix is kept in the resulting
* dictionary. As side-effect, a key that matches the prefix exactly
* will also be copied. If false, the resulting dictionary's keys are
* shortened by the prefix. An exact prefix match will not be copied,
* as it would result in an empty string key.
* @return a property dictionary matching the filter key. May be
* an empty dictionary, if no prefix matches were found.
*
* @see #getProperty( String ) is used to assemble matches
*/
public static Properties matchingSubset( Properties properties, String prefix, boolean keepPrefix ) {
Properties result = new Properties();
// sanity check
if ( prefix == null || prefix.length() == 0 ) return result;
String prefixMatch; // match prefix strings with this
String prefixSelf; // match self with this
if ( prefix.charAt(prefix.length()-1) != '.' ) {
// prefix does not end in a dot
prefixSelf = prefix;
prefixMatch = prefix + '.';
} else {
// prefix does end in one dot, remove for exact matches
prefixSelf = prefix.substring( 0, prefix.length()-1 );
prefixMatch = prefix;
}
// POSTCONDITION: prefixMatch and prefixSelf are initialized!
// now add all matches into the resulting properties.
// Remark 1: #propertyNames() will contain the System properties!
// Remark 2: We need to give priority to System properties. This is done
// automatically by calling this class's getProperty method.
String key;
for ( Enumeration e = properties.propertyNames(); e.hasMoreElements(); ) {
key = (String) e.nextElement();
if ( keepPrefix ) {
// keep full prefix in result, also copy direct matches
if ( key.startsWith(prefixMatch) || key.equals(prefixSelf) )
result.setProperty( key,
properties.getProperty(key) );
} else {
// remove full prefix in result, dont copy direct matches
if ( key.startsWith(prefixMatch) )
result.setProperty( key.substring( prefixMatch.length() ),
properties.getProperty(key) );
}
}
// done
return result;
}
/**
* Print out the property list onto the specified stream. This method
* is useful for debugging, and meant for debugging.
*
* @param out an output stream
* @throws ClassCastException if any key is not a string.
*
* @see java.util.Properties#list( PrintStream )
*/
public void list(PrintStream out)
{
m_props.list(out);
}
public static void main( String[] args )
throws java.io.IOException
{
CommonProperties cp = null;
if ( args.length > 0 ) cp = CommonProperties.nonSingletonInstance(args[0]);
else cp = CommonProperties.instance();
cp.list( System.out );
}
/**
* Returns the clone of the object.
*
* @return the clone
*/
public Object clone(){
CommonProperties props;
try{
//this will do a shallow clone for all member variables
//that is fine for the string variables
props = ( CommonProperties ) super.clone();
//do a deep clone for the underlying properties
//can be expensive
props.m_props = new Properties();
for( Object key: this.m_props.keySet() ){
props.m_props.put(key, this.m_props.get(key));
}
}
catch( CloneNotSupportedException e ){
//somewhere in the hierarch chain clone is not implemented
throw new RuntimeException("Clone not implemented in the base class of " + this.getClass().getName(),
e );
}
return props;
}
/**
* Removes a property from the soft state.
*
* @param key the key
*
* @return the corresponding value if key exits, else null
*/
public String removeProperty(String key) {
return (String) this.m_props.remove(key);
}
}