/*
* This file is part of Bracket Properties
* Copyright 2011 David R. Smith
*
*/
package asia.redact.bracket.properties;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.util.BitSet;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import asia.redact.bracket.properties.alt.DotPropertiesParser;
import asia.redact.bracket.properties.line.LineScanner;
import asia.redact.bracket.properties.mgmt.Attributes;
import asia.redact.bracket.properties.mgmt.LoadList;
import asia.redact.bracket.properties.mgmt.PropertiesReference;
/**
* <pre>
* In the java.util package, Properties is not an Interface. Bracket Properties
* has one, which allows (among other things) for both a standard and a sorted implementation. The
* standard implementation is backed by a LinkedHashMap, which keeps insertion
* order intact. This is a critical issue for non-trivial use of Properties files.
*
* This interface is also home to the static Factory, which is the supported way to
* instantiate a Bracket Properties object.
*
* </pre>
* @author Dave
*
*/
public interface Properties extends Serializable {
/**
* Can be used to get direct access to the Entry data structures
* @return
*/
public Map<String, ValueModel> getPropertyMap();
/**
* Get the value.
* @param key
* @return
*/
public String get(String key);
/**
* Coerce to an integer value. Obviously this works better if the value is actually an integer.
* @param key
* @return
*/
public int intValue(String key);
/**
* Coerce to a long value. Obviously this works better if the value is actually a long.
* @param key
* @return
*/
public long longValue(String key);
/**
* Date value here is assumed to be a long
* @param key
* @return
*/
public java.util.Date dateValue(String key);
/**
* Just syntactical sugar to use a SimpleDateFormat
*
* @param key
* @param format
* @return
* @throws ParseException
*/
public java.util.Date dateValue(String key, String format) throws ParseException;
public List<String> listValue(String key);
public List<String> listValue(String key, String delimiter);
public BitSet bitsetValue(String key);
public BigInteger bigValue(String key);
public BigDecimal bigDecimalValue(String key);
public Object beanValue(Class<?> clazz, String keyBase);
/**
* <pre>Get the properties as a tree of nodes. For example,
*
* a.b.c=something
* a.b.c.d=something else
*
* looks like
*
* a
* b
* c - something
* d - something else
*
* This method is identical in results to getTree(regex) where the regex
* is "\\.". That is, the separator token in the key is a full stop
*
* Obviously this works better if your keys are delimited by dot characters
* </pre>
*
* @return
*/
public Node getTree();
/**
* <pre>
* Get the properties as a tree of nodes with a selector
*
* a.b.c=something
* a.b.c.d=something else
* a.b.c.e.f=item
* a.b.c.e=item2
* </pre>
*
*
*/
public Node getTree(GroupParams params);
/**
* Get the list of comments, return an empty list if none
*
* @param key
* @return
*/
public List<String> getComments(String key);
/**
* The char found in the parse, normally '='
*
* @param key
* @return
*/
public char getSeparator(String key);
/**
* Add the key and value or values. Useful with multi-line entries
*
* @param key
* @param values
*/
public void put(String key, String ... values);
/**
* Number of entries in the underlying map
*
* @return
*/
public int size();
/**
* remove all entries from the underlying map
*/
public void clear();
/**
* <pre>
* get(key) will throw a RuntimeException if the key does not
* exist. This method can be used to test for a key prior to
* calling get().
*
* Returns true if the underlying map has this key
*
* </pre>
* @param key
* @return
*/
public boolean containsKey(String key);
/**
* Returns true if the key exists and has a non-empty value
*
* @param key
* @return
*/
public boolean hasValue(String key);
/**
* Overwrite existing keys with the new ones, keep those existing ones that don't collide
* This operation is non-destructive on the input
* does not concatenate comments
*
* @param props
* @return the merged properties
*/
public Properties merge(Properties props);
/**
* Overwrite existing keys with the new ones, keep those existing ones that don't collide
* This operation is non-destructive on the input
*
* @param props
* @return the merged properties
*/
public Properties merge(Properties props, boolean mergeComments);
/**
* Cause a graph to become the contents of the properties file. Destructive
* to current entries, so this is not very useful yet
*
*
* @param rootNode
*/
public void synchronize(Node rootNode);
/**
* Explicit content type and charset support
*
* @return
*/
public ContentType getContentType();
public void setContentType(ContentType contentType);
/**
* Convenience method. Put all of these properties into the System properties using System.setProperty() call on each one
*
* Not easily reversible, use with great care
*
*/
public void mergeIntoSystemProperties();
/**
* <pre>
* This method relies on the convention of using numbers at
* the end of a property key to represent a list member. The total
* list is the set of all similar keys with key as the base
* and dot delimited integers at the end. Suppose the following
* (from the Tanukisoft wrapper.conf):
*
* wrapper.java.classpath.1=../lib/wrapper.jar
* wrapper.java.classpath.2=../lib/myapp.jar
* wrapper.java.classpath.3=../lib/mysql.jar
* wrapper.java.classpath.4=../classes
*
* Then calling getList("wrapper.java.classpath") would
* return all the values above, in numeric order, as a List.
*
* If key does not exist but numbered properties exist, the
* key is synthesized. if no numbered properties exist, an
* empty list is returned. If the key does not exist and no numbered
* keys exist, the method returns an empty list
*
* Numbers need not be sequential
*
* </pre>
* @param key
* @return
*/
public List<String> getList(String key);
/**
* <pre>
* create property keys and values based on a root key and a list in the form:
*
* rootKey.0= list.get(0);
* rootKey.1= list.get(1);
*
* and so on. This method is a complement to using getList(rootKey).
*
* </pre>
* @param list
* @param rootKey
*/
public void putList(List<String> list, String rootKey);
/**
* Return keys which match the pattern keyBase.0, keyBase.1, keyBase.2, etc.
* The list returned is in natural sorted order.
*
* @param keyBase
* @return
*/
public List<String> getListKeys(String keyBase);
/**
* Specialty method used with a map that has been serialized in the form keyBase dot integer dot k|v
* For example,
*
* 0.0.k=a key
* 0.0.v=a value
*
*/
public List<String> getMapKeys(String keyBase);
/**
* Like containsKey but matching on the partial
*
* @param partial
* @return
*/
public boolean hasKeyLike(String partial);
/**
* <pre>
* Mode is the available combinations of lexer and parser
*
* BasicToken - PropertiesLexer and PropertiesParser.
* Input is a String and a list of tokens is created, then the list
* is parsed. Trivial. Possibly good for small properties files
*
* Compatibility - PropertiesLexer and PropertiesParser.
* Same parser as above but an attempt is made to retain
* java.util.Properties compatibility in the parse.
*
* Line - LineScanner and PropertiesParser2.
* Uses the LineScanner which is essentially a BufferedReader, and there is no
* separate token list, parser works directly off lines. Should work best for
* larger properties files. Probably the best option full stop.
*
* Usage:
*
* Properties.Factory.Mode = Properties.Mode.StreamingToken;
* Properties props = Properties.Factory.getInstance(reader);
*
* </pre>
*
* @author Dave
*
*/
public enum Mode {
BasicToken, Compatibility, Line, Explicit;
}
/**
* <pre>
* The default mode is the trivial memory mode, which is BasicToken.
*
* You can change the Mode of the factory globally by setting it to a different value like this:
*
* Properties.mode = Mode.Line;
*
* which changes the mode (and thus the parser/lexer) to Line.
*
* </pre>
*
* @author Dave
*
*/
public static final class Factory {
public static Mode mode = Mode.BasicToken;
// private final static Logger log = Logger.getLogger("asia.redact.bracket.properties.Properties.Factory");
public static Properties getInstance(){
return new PropertiesImpl();
}
public synchronized static Properties getInstance(URL url){
try {
return Factory.getInstance(url.openStream());
} catch (IOException e) {
throw new RuntimeException("IOException caught",e);
}
}
public synchronized static Properties getInstance(Reader reader){
switch(mode){
case BasicToken: return new PropertiesImpl(reader);
case Compatibility: {
PropertiesLexer lexer = new PropertiesLexer(reader);
lexer.lex();
List<PropertiesToken> list = lexer.getList();
PropertiesImpl props = new PropertiesImpl();
props.setContentType(ContentType.getCompatibilityContentType());
PropertiesParser p = new PropertiesParser(list, props);
p.setTrimValues(true);
p.parse();
return props;
}
case Line:{
LineScanner lexer = new LineScanner(reader);
PropertiesImpl props = new PropertiesImpl();
new PropertiesParser2(lexer,props).parse();
return props;
}
default:
break;
}
return new PropertiesImpl(reader);
}
public synchronized static Properties getInstance(File file, Charset charset){
FileInputStream stream=null;
try {
stream = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(stream,charset);
return getInstance(reader);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}finally{
if(stream !=null)
try {
stream.close();
} catch (IOException e) {}
}
}
public synchronized static Properties getInstance(InputStream in){
switch(mode){
case BasicToken: return new PropertiesImpl(in);
case Compatibility: {
PropertiesLexer lexer = new PropertiesLexer(in);
lexer.lex();
List<PropertiesToken> list = lexer.getList();
PropertiesImpl props = new PropertiesImpl();
props.setContentType(ContentType.getCompatibilityContentType());
PropertiesParser p = new PropertiesParser(list, props);
p.setTrimValues(true);
p.parse();
return props;
}
case Line:{
LineScanner lexer = new LineScanner(new InputStreamReader(in));
PropertiesImpl props = new PropertiesImpl();
new PropertiesParser2(lexer,props).parse();
return props;
}
default:
break;
}
return new PropertiesImpl(in);
}
public synchronized static Properties getInstance(InputStream in, Charset charset){
switch(mode){
case BasicToken: return new PropertiesImpl(in);
case Compatibility: {
PropertiesLexer lexer = new PropertiesLexer(new InputStreamReader(in,charset));
lexer.lex();
List<PropertiesToken> list = lexer.getList();
PropertiesImpl props = new PropertiesImpl();
props.setContentType(ContentType.getCompatibilityContentType());
PropertiesParser p = new PropertiesParser(list, props);
p.setTrimValues(true);
p.parse();
return props;
}
case Line:{
LineScanner lexer = new LineScanner(new InputStreamReader(in, charset));
PropertiesImpl props = new PropertiesImpl();
new PropertiesParser2(lexer,props).parse();
return props;
}
default:
break;
}
return new PropertiesImpl(in, charset);
}
/**
* Load from a legacy Properties file object
*
* @param legacy
* @return
*/
public static Properties getInstance(java.util.Properties legacy){
return new PropertiesImpl(legacy);
}
/**<pre>
* Similar to the behavior of a ResourceBundle.
*
* baseName is something like a.b.c.MyProperty which with Locale.AU will be
* a search path like /a.b.c.MyProperty_en_AU.properties
*
* </pre>
*
* @param baseName
* @param locale
* @return
*/
public static Properties getInstance(String baseName, Locale locale) {
LocaleStringBuilder builder = new LocaleStringBuilder(baseName,locale);
List<String> list = builder.getSearchStrings();
PropertiesImpl base = new PropertiesImpl();
for(String s: list){
InputStream in = null;
try {
in = Thread.currentThread().getClass().getResourceAsStream(s);
if(in != null){
base.merge(Properties.Factory.getInstance(in));
}
}finally{
if(in != null)
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return base;
}
/**
* The input file must have been generated by OutputAdapter.writeAsXML(Writer) or meet the same
* requirements as regards form. This is not yet documented but looking at the test case files you can
* figure it out.
*
* @throws RuntimeException if it fails due to I/O
*
* @param file
* @return
*/
public synchronized static Properties getInstanceFromXML(File file, Charset charset) {
FileInputStream in = null;
try {
in = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(in, charset);
BufferedReader breader = new BufferedReader(reader);
ParseXML parser = new ParseXML();
parser.parse(breader);
return parser.getProps();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* If Properties.Factory.mode == Mode.Compatibility, will expect ISO-8859-1. Otherwise,
* UTF-8 is used.
*
* @param file
* @return
*/
public synchronized static Properties getInstanceFromXML(File file) {
FileInputStream in = null;
try {
in = new FileInputStream(file);
Charset charset = null;
if(Properties.Factory.mode == Mode.Compatibility){
charset = Charset.forName("ISO-8859-1");
}else{
charset = Charset.forName("UTF-8");
}
InputStreamReader reader = new InputStreamReader(in, charset);
BufferedReader breader = new BufferedReader(reader);
ParseXML parser = new ParseXML();
parser.parse(breader);
return parser.getProps();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
public synchronized static Properties sortedInstance(Properties props){
SortedPropertiesImpl impl = new SortedPropertiesImpl();
return impl.merge(props);
}
public synchronized static Properties sortedInstance(Properties props, Comparator<String> comp){
SortedPropertiesImpl impl = new SortedPropertiesImpl(comp);
return impl.merge(props);
}
public synchronized static Properties getDotInstance(Reader reader){
LineScanner lexer = new LineScanner(reader);
DotPropertiesParser p = new DotPropertiesParser(lexer);
p.parse();
Properties props = p.getProperties();
props.setContentType(ContentType.getDotPropertiesContentType());
return props;
}
public synchronized static Properties getDotInstance(InputStream in){
LineScanner lexer = new LineScanner(new InputStreamReader(in));
DotPropertiesParser p = new DotPropertiesParser(lexer);
p.parse();
Properties props = p.getProperties();
props.setContentType(ContentType.getDotPropertiesContentType());
return props;
}
public synchronized static Properties loadReferences(List<PropertiesReference> refs){
Attributes attribs = new Attributes();
attribs.warnOnNoPropertiesFileExtension = true;
attribs.useCompatibilityMode = true;
attribs.locale = Locale.getDefault();
return loadReferences(refs, attribs);
}
/**
*
* <pre>
* Given a set of paths in the OS, load the files one by one into the Properties. This method
* relies on merge(), which overwrites if it finds a duplicate. So, suppose you have the following
*
* <application>/WEB-INF/myapp.properties
* <USER_HOME>/.ext/passwords.properties
*
* Then you would do the following:
*
* List<String> list = new List<String>();
* list.add("C://MyStuff/tomcat/webapps/mywar/WEB-INF/myapp.properties");
* list.add("C://Users/dsmith/.ext/passwords.properties");
* Properties props = Properties.Factory.loadReferences(list,attribs);
*
* props will now have properties from myapp.properties and written on top of those will be
* contents of passwords.properties
*
* The loadReferences methods support ResourceBundle style localization via attributes.locale.
* Set this to the desired locale. Bracket goes beyond ResourceBundles by supporting
* *externalized* localization.
*
* </pre>
*
* */
public synchronized static Properties loadReferences(List<PropertiesReference> refs, Attributes attribs){
LoadList list = new LoadList(attribs);
for(PropertiesReference ref: refs){
list.addReference(ref);
}
list.load();
return list.getProps();
}
}
}