/*
* Helma License Notice
*
* The contents of this file are subject to the Helma License
* Version 2.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://adele.helma.org/download/helma/license.txt
*
* Copyright 1998-2003 Helma Software. All Rights Reserved.
*
* $RCSfile$
* $Author: hannes $
* $Revision: 9626 $
* $Date: 2009-04-17 16:49:26 +0200 (Fre, 17. Apr 2009) $
*/
package helma.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import helma.framework.core.*;
import helma.framework.repository.Resource;
import helma.framework.repository.Repository;
/**
* A property dictionary that is updated from property resources
*/
public class ResourceProperties extends Properties {
// Delay between checks
private final long CACHE_TIME = 1500L;
// Default properties. Note that in contrast to java.util.Properties,
// defaultProperties are copied statically to ourselves in update(), so
// there's no need to check them in retrieval methods.
protected ResourceProperties defaultProperties;
// Defines wether keys are case-sensitive or not
private boolean ignoreCase = true;
// Cached checksum of last check
private long lastChecksum = 0;
// Time of last check
private long lastCheck = 0;
// Time porperties were last modified
private long lastModified = System.currentTimeMillis();
// Application where to fetch additional resources
private Application app;
// Name of possible resources to fetch from the applications's repositories
private String resourceName;
// Sorted map of resources
private Set resources;
// lower case key to original key mapping for case insensitive lookups
private Properties keyMap = new Properties();
// prefix for sub-properties
private String prefix;
// parent properties for sub-properties
private ResourceProperties parentProperties;
/**
* Constructs an empty ResourceProperties
* Resources must be added manually afterwards
*/
public ResourceProperties() {
// TODO: we can't use TreeSet because we don't have the app's resource comparator
// Since resources don't implement Comparable, we can't add them to a "naked" TreeSet
// As a result, resource ordering is random when updating.
resources = new LinkedHashSet();
}
/**
* Constructs an empty ResourceProperties
* Resources must be added manually afterwards
*/
public ResourceProperties(Application app) {
resources = new TreeSet(app.getResourceComparator());
}
/**
* Constructs a ResourceProperties retrieving resources from the given
* application using the given name to fetch resources
* @param app application to fetch resources from
* @param resourceName name to use when fetching resources from the application
*/
public ResourceProperties(Application app, String resourceName) {
this.app = app;
this.resourceName = resourceName;
resources = new TreeSet(app.getResourceComparator());
}
/**
* Constructs a ResourceProperties retrieving resources from the given
* application using the given name to fetch resources and falling back
* to the given default properties
* @param app application to fetch resources from
* @param resourceName name to use when fetching resources from the application
* @param defaultProperties default properties
*/
public ResourceProperties(Application app, String resourceName,
ResourceProperties defaultProperties) {
this(app, resourceName);
this.defaultProperties = defaultProperties;
forceUpdate();
}
/**
* Constructs a ResourceProperties retrieving resources from the given
* application using the given name to fetch resources and falling back
* to the given default properties
* @param app application to fetch resources from
* @param resourceName name to use when fetching resources from the application
* @param defaultProperties default properties
* @param ignoreCase ignore case for property keys, setting all keys to lower case
*/
public ResourceProperties(Application app, String resourceName,
ResourceProperties defaultProperties,
boolean ignoreCase) {
this(app, resourceName);
this.defaultProperties = defaultProperties;
this.ignoreCase = ignoreCase;
forceUpdate();
}
/**
* Constructs a properties object containing all entries where the key matches
* the given string prefix from the source map to the target map, cutting off
* the prefix from the original key.
* @see #getSubProperties(String)
* @param parentProperties the parent properties
* @param prefix the property name prefix
*/
private ResourceProperties(ResourceProperties parentProperties, String prefix) {
this.parentProperties = parentProperties;
this.prefix = prefix;
resources = new LinkedHashSet();
setIgnoreCase(parentProperties.ignoreCase);
forceUpdate();
}
/**
* Updates the properties regardless of an actual need
*/
private void forceUpdate() {
lastChecksum = -1;
update();
}
/**
* Sets the default properties and updates all properties
* @param defaultProperties default properties
*/
public void setDefaultProperties(ResourceProperties defaultProperties) {
this.defaultProperties = defaultProperties;
update();
}
/**
* Adds a resource to the list of resources and updates all properties if
* needed
* @param resource resource to add
*/
public void addResource(Resource resource) {
if (resource != null && !resources.contains(resource)) {
resources.add(resource);
forceUpdate();
}
}
/**
* Removes a resource from the list of resources and updates all properties
* if needed
* @param resource resource to remove
*/
public void removeResource(Resource resource) {
if (resources.contains(resource)) {
resources.remove(resource);
forceUpdate();
}
}
/**
* Get an iterator over the properties' resources
* @return iterator over the properties' resources
*/
public Iterator getResources() {
return resources.iterator();
}
/**
* Updates all properties if there is a need to update
*/
public synchronized void update() {
// set lastCheck first to reduce risk of recursive calls
lastCheck = System.currentTimeMillis();
if (getChecksum() != lastChecksum) {
// First collect properties into a temporary collection,
// in a second step copy over new properties,
// and in the final step delete properties which have gone.
ResourceProperties temp = new ResourceProperties();
temp.setIgnoreCase(ignoreCase);
// first of all, properties are load from default properties
if (defaultProperties != null) {
defaultProperties.update();
temp.putAll(defaultProperties);
}
// next we try to load properties from the application's
// repositories, if we belong to any application
if (resourceName != null) {
Iterator iterator = app.getRepositories().iterator();
while (iterator.hasNext()) {
try {
Repository repository = (Repository) iterator.next();
Resource res = repository.getResource(resourceName);
if (res != null && res.exists()) {
InputStream in = res.getInputStream();
temp.load(in);
in.close();
}
} catch (IOException iox) {
iox.printStackTrace();
}
}
}
// if these are subproperties, reload them from the parent properties
if (parentProperties != null && prefix != null) {
parentProperties.update();
Iterator it = parentProperties.entrySet().iterator();
int prefixLength = prefix.length();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String key = entry.getKey().toString();
if (key.regionMatches(ignoreCase, 0, prefix, 0, prefixLength)) {
temp.put(key.substring(prefixLength), entry.getValue());
}
}
}
// at last we try to load properties from the resource list
if (resources != null) {
Iterator iterator = resources.iterator();
while (iterator.hasNext()) {
try {
Resource res = (Resource) iterator.next();
if (res.exists()) {
InputStream in = res.getInputStream();
temp.load(in);
in.close();
}
} catch (IOException iox) {
iox.printStackTrace();
}
}
}
// Copy over new properties ...
putAll(temp);
// ... and remove properties which have been removed.
Iterator it = super.keySet().iterator();
while (it.hasNext()) {
if (!temp.containsKey(it.next())) {
it.remove();
}
}
// copy new up-to-date keyMap to ourself
keyMap = temp.keyMap;
lastChecksum = getChecksum();
lastCheck = lastModified = System.currentTimeMillis();
}
}
/**
* Extract all entries where the key matches the given string prefix from
* the source map to the target map, cutting off the prefix from the original key.
* The ignoreCase property is inherited and also considered when matching keys
* against the prefix.
*
* @param prefix the string prefix to match against
* @return a new subproperties instance
*/
public ResourceProperties getSubProperties(String prefix) {
if (prefix == null) {
throw new NullPointerException("prefix");
}
return new ResourceProperties(this, prefix);
}
/**
* Checks wether the given object is in the value list
* @param value value to look for
* @return true if the value is found in the value list
*/
public boolean contains(Object value) {
if ((System.currentTimeMillis() - lastCheck) > CACHE_TIME) {
update();
}
return super.contains(value.toString());
}
/**
* Checks wether the given object is in the key list
* @param key key to look for
* @return true if the key is found in the key list
*/
public boolean containsKey(Object key) {
if ((System.currentTimeMillis() - lastCheck) > CACHE_TIME) {
update();
}
if (ignoreCase) {
return keyMap.containsKey(key.toString().toLowerCase());
} else {
return super.containsKey(key.toString());
}
}
/**
* Returns an enumeration of all values
* @return values enumeration
*/
public Enumeration elements() {
if ((System.currentTimeMillis() - lastCheck) > CACHE_TIME) {
update();
}
return super.elements();
}
/**
* Returns a value in this list fetched by the given key
* @param key key to use for fetching the value
* @return value belonging to the given key
*/
public Object get(Object key) {
if ((System.currentTimeMillis() - lastCheck) > CACHE_TIME) {
update();
}
String strkey = key.toString();
if (ignoreCase) {
strkey = keyMap.getProperty(strkey.toLowerCase());
if (strkey == null)
return null;
}
return super.get(strkey);
}
/**
* Returns the date the resources were last modified
* @return last modified date
*/
public long lastModified() {
if ((System.currentTimeMillis() - lastCheck) > CACHE_TIME) {
update();
}
return lastModified;
}
/**
* Returns a checksum for all resources
* @return checksum
*/
public long getChecksum() {
long checksum = 0;
if (resourceName != null) {
Iterator iterator = app.getRepositories().iterator();
while (iterator.hasNext()) {
Repository repository = (Repository) iterator.next();
Resource resource = repository.getResource(resourceName);
if (resource != null) {
checksum += resource.lastModified();
}
}
}
if (resources != null) {
Iterator iterator = resources.iterator();
while (iterator.hasNext()) {
checksum += ((Resource) iterator.next()).lastModified();
}
}
if (defaultProperties != null) {
checksum += defaultProperties.getChecksum();
}
return checksum;
}
/**
* Returns a value in the list fetched by the given key or a default value
* if no corresponding key is found
* @param key key to use for fetching the value
* @param defaultValue default value to return if key is not found
* @return spiecific value or default value if not found
*/
public String getProperty(String key, String defaultValue) {
if ((System.currentTimeMillis() - lastCheck) > CACHE_TIME) {
update();
}
if (ignoreCase) {
key = keyMap.getProperty(key.toLowerCase());
if (key == null)
return defaultValue;
}
return super.getProperty(key, defaultValue);
}
/**
* Returns a value in this list fetched by the given key
* @param key key to use for fetching the value
* @return value belonging to the given key
*/
public String getProperty(String key) {
if ((System.currentTimeMillis() - lastCheck) > CACHE_TIME) {
update();
}
if (ignoreCase) {
key = keyMap.getProperty(key.toLowerCase());
if (key == null)
return null;
}
return super.getProperty(key);
}
/**
* Checks wether the properties list is empty
* @return true if the properties list is empty
*/
public boolean isEmpty() {
if ((System.currentTimeMillis() - lastCheck) > CACHE_TIME) {
update();
}
return super.isEmpty();
}
/**
* Checks wether case-sensitivity is ignored for keys
* @return true if case-sensitivity is ignored for keys
*/
public boolean isIgnoreCase() {
return ignoreCase;
}
/**
* Returns an enumeration of all keys
* @return keys enumeration
*/
public Enumeration keys() {
if ((System.currentTimeMillis() - lastCheck) > CACHE_TIME) {
update();
}
return super.keys();
}
/**
* Returns a set of all keys
* @return keys set
*/
public Set keySet() {
if ((System.currentTimeMillis() - lastCheck) > CACHE_TIME) {
update();
}
return super.keySet();
}
/**
* Puts a new key-value pair into the properties list
* @param key key
* @param value value
* @return the old value, if an old value got replaced
*/
public Object put(Object key, Object value) {
if (value instanceof String) {
value = ((String) value).trim();
}
String strkey = key.toString();
if (ignoreCase) {
keyMap.put(strkey.toLowerCase(), strkey);
}
return super.put(strkey, value);
}
/**
* Removes a key-value pair from the properties list
* @param key key
* @return the old value
*/
public Object remove(Object key) {
String strkey = key.toString();
if (ignoreCase) {
strkey = (String) keyMap.remove(strkey.toLowerCase());
if (strkey == null)
return null;
}
return super.remove(strkey);
}
/**
* Changes how keys are handled
* @param ignore true if to ignore case-sensitivity for keys
*/
public void setIgnoreCase(boolean ignore) {
if (!super.isEmpty()) {
throw new RuntimeException("setIgnoreCase() can only be called on empty Properties");
}
ignoreCase = ignore;
}
/**
* Returns the number of peroperties in the list
* @return number of properties
*/
public int size() {
if ((System.currentTimeMillis() - lastCheck) > CACHE_TIME) {
update();
}
return super.size();
}
/**
* Overwrite clear() to also empty the key map.
*/
public void clear() {
keyMap.clear();
super.clear();
}
/**
* Returns a string-representation of the class
* @return string
*/
public String toString() {
return super.toString();
}
}