/*
* 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.ode.utils;
import org.apache.commons.collections.map.MultiKeyMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.xml.namespace.QName;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Collections;
import java.util.Iterator;
/**
* This class load a regular property file in {@link java.util.Properties} instance. The main feature is that property can
* be chained in three levels. Then when querying for a property, if it's not found in the deepest level,
* the parent will be queryed and so on.
* <p/>
* A prefix must be defined to discriminate the property name and the level-1, level-2 names. The default prefix is {@link #ODE_PREFFIX}.
* <p/>
* Properties must respect the following pattern: [level1.[level2.]prefix.]property
* <p/>
* A concrete use case could be the definition of properties for wsdl services and ports.
* <br/>Level 0 would be: values common to all services and ports.
* <br/>Level 1: values common to a given service.
* <br/>Level 2: values common to a given port.
* <p/>
* For instance, if the property file looks like this:
* <pre>
* timeout=40000
* film-service.port-of-cannes.ode.timeout=50000
* <p/>
* max-redirects=30
* brel-service.ode.max-redirects=40
* brel-service.port-of-amsterdam.ode.max-redirects=60
* </pre>
* The following values may be expected:
* <pre>
* getProperty("max-redirects") => 30
* getProperty("brel-service", "max-redirects") => 40
* getProperty("brel-service", "port-of-amsterdam", "max-redirects") => 60
* <p/>
* getProperty("film-service", "timeout") => 40000
* getProperty("film-service", "port-of-cannes", "timeout") => 50000
* getProperty("brel-service", "port-of-amsterdam", "timeout") => 40000
* </pre>
* <p/>
* Values may contain some environment variables. For instance, message=You're using ${java.version}.
* <p/>
* This class is not thread-safe.
*
* @author <a href="mailto:midon@intalio.com">Alexis Midon</a>
*/
public class HierarchicalProperties {
private static final Log log = LogFactory.getLog(HierarchicalProperties.class);
public static final String ODE_PREFFIX = "ode";
// the raw properties as of loaded from the filesystem
private Properties props = new Properties();
private Map<String, String> aliases = new HashMap<String, String>();
private File file;
private String prefix;
private String dotted_prefix;
/*
This map contains ChainedMap instances chained according to the service and/or port they are associated with.
All ChainedMap instances has a common parent.
The ChainedMap instances are chained to each others so that if a property is not found for [service, port],
the ChainedMap associated to [service] will be queried, and if still not found, then the common parent.
The ChainedMap instance common to all services and ports is associated to the [null, null] key.
ChainedMap instance common to all ports of a given service is associated to [service, null].
ChainedMap instance of a given service, port couple is associated to [service, port].
The ChainedMap instances contain string values as loaded from the filesystem.
*/
private MultiKeyMap hierarchicalMap = new MultiKeyMap();
// map used to cache immutable versions of the maps
private transient MultiKeyMap cacheOfImmutableMaps = new MultiKeyMap();
/**
* @param file the property file to be loaded. The file may not exist.
* But if the file exists it has to be a file (not a directory), otherwhise an IOException is thrown.
* @param prefix the property prefix
* @throws IOException
*/
public HierarchicalProperties(File file, String prefix) throws IOException {
this.file = file;
this.prefix = prefix;
this.dotted_prefix = "." + prefix + ".";
loadFile();
}
public HierarchicalProperties(File file) throws IOException {
this(file, ODE_PREFFIX);
}
/**
* Clear all existing content, read the file and parse each property. Simply logs a message and returns if the file does not exist.
*
* @throws IOException if the file is a Directory
*/
public void loadFile() throws IOException {
if (!file.exists()) {
if (log.isDebugEnabled()) log.debug("File does not exist [" + file + "] Properties will be empty.");
return;
}
// #1. clear all existing content
clear();
// #2. read the file
FileInputStream fis = new FileInputStream(file);
try {
if (log.isDebugEnabled()) log.debug("Loading property file: " + file);
props.load(fis);
} finally {
fis.close();
}
// #3. put the root map
hierarchicalMap.put(null, null, new ChainedMap());
// #4. process each property
for (Iterator it = props.entrySet().iterator(); it.hasNext();) {
Map.Entry e = (Map.Entry) it.next();
String key = (String) e.getKey();
String value = (String) e.getValue();
// replace any env variables by its value
value = SystemUtils.replaceSystemProperties(value);
props.put(key, value);
if (key.startsWith("alias.")) {
final String alias = key.substring("alias.".length(), key.length());
if(log.isDebugEnabled()) log.debug("Alias found: "+alias+" -> "+value);
aliases.put(value, alias);
} else {
// parse the property name
String[] info = parseProperty((String) key);
String nsalias = info[0];
String service = info[1];
String port = info[2];
String targetedProperty = info[3];
QName qname = nsalias != null ? new QName(nsalias, service) : null;
// get the map associated to this port
ChainedMap p = (ChainedMap) hierarchicalMap.get(qname, port);
if (p == null) {
// create it if necessary
// get the associated service map
ChainedMap s = (ChainedMap) hierarchicalMap.get(qname, null);
if (s == null) {
// create the service map if necessary, the parent is the root map.
s = new ChainedMap(getRootMap());
// put it in the multi-map
hierarchicalMap.put(qname, null, s);
}
// create the map itself and link it to theservice map
p = new ChainedMap(s);
// put it in the multi-map
hierarchicalMap.put(qname, port, p);
}
// save the key/value in its chained map
p.put(targetedProperty, value);
}
}
}
/**
* Clear all content. If {@link #loadFile()} is not invoked later, all returned values will be null.
*/
public void clear() {
props.clear();
aliases.clear();
hierarchicalMap.clear();
cacheOfImmutableMaps.clear();
}
protected ChainedMap getRootMap() {
Object o = hierarchicalMap.get(null, null);
if (o == null) {
o = new ChainedMap();
hierarchicalMap.put(null, null, o);
}
return (ChainedMap) o;
}
public Map getProperties(String serviceNamespaceURI, String serviceLocalPart) {
return getProperties(new QName(serviceNamespaceURI, serviceLocalPart));
}
/**
* @param service
* @return a map containing all the properties for the given service.
* @see #getProperties(String, String)
*/
public Map getProperties(QName service) {
return getProperties(service, null);
}
public Map getProperties(String serviceNamespaceURI, String serviceLocalPart, String port) {
return getProperties(new QName(serviceNamespaceURI, serviceLocalPart), port);
}
/**
* Return a map containing all the properties for the given port. The map is an immutable snapshot of the properties.
* Meaning that futur changes to the properties will NOT be reflected in the returned map.
*
* @param service
* @param port
* @return a map containing all the properties for the given port
*/
public Map getProperties(QName service, String port) {
// no need to go further if no properties
if (hierarchicalMap.isEmpty()) return Collections.EMPTY_MAP;
service = resolveAlias(service);
// else check the cache of ChainedMap already converted into immutable maps
Map cachedMap = (Map) this.cacheOfImmutableMaps.get(service, port);
if (cachedMap != null) {
return cachedMap;
}
// else get the corresponding ChainedMap and convert it into a Map
ChainedMap cm = (ChainedMap) hierarchicalMap.get(service, port);
// if this port is not explicitly mentioned in the multimap, get the default values.
if (cm == null) {
cm = (ChainedMap) hierarchicalMap.get(service, null);
if (cm == null) {
// return the cached version of the root map
return getProperties((QName) null, null);
}
}
Map snapshotMap = new HashMap(cm.size() * 15 / 10);
for (Object key : cm.keySet()) {
snapshotMap.put(key, cm.get(key));
}
snapshotMap = Collections.unmodifiableMap(snapshotMap);
// put it in cache to avoid creating one map at each invocation
this.cacheOfImmutableMaps.put(service, port, snapshotMap);
return snapshotMap;
}
public String getProperty(String property) {
return (String) getRootMap().get(property);
}
public String getProperty(String serviceNamespaceURI, String serviceLocalPart, String property) {
return getProperty(new QName(serviceNamespaceURI, serviceLocalPart), property);
}
public String getProperty(QName service, String property) {
return getProperty(service, null, property);
}
public String getProperty(String serviceNamespaceURI, String serviceLocalPart, String port, String property) {
return getProperty(new QName(serviceNamespaceURI, serviceLocalPart), port, property);
}
public String getProperty(QName service, String port, String property) {
ChainedMap cm = (ChainedMap) hierarchicalMap.get(resolveAlias(service), port);
// if this port is not explicitly mentioned in the multimap, get the default values.
if (cm == null) cm = getRootMap();
return (String) cm.get(property);
}
public String getPrefix() {
return prefix;
}
private QName resolveAlias(QName service) {
if (service != null && aliases.containsKey(service.getNamespaceURI())) {
return new QName(aliases.get(service.getNamespaceURI()), service.getLocalPart());
}
return service;
}
private String[] parseProperty(String property) {
// aliaas ns, service, port, targeted property
String[] res = new String[4];
int index = property.indexOf(dotted_prefix);
if (index <= 0) {
// assume there is no service/port prefixed, no need to go further
res[3] = property;
} else {
res[3] = property.substring(index + dotted_prefix.length()); // targeted property
String prefix = property.substring(0, index);
String[] t = prefix.split("\\.");
if (t.length != 2 && t.length != 3) {
throw new IllegalArgumentException("Invalid property name:" + property + " Expected pattern: [nsalias.service.[port.]" + prefix + ".]property");
}
if (t.length >= 2) {
res[0] = t[0]; // ns alias
res[1] = t[1]; // service name
}
if (t.length > 2) {
res[2] = t[2]; // port name
}
}
return res;
}
/**
* Link two Maps instances in a parent-child relation. Meaning that if a key is looked up but not found on the child,
* then the key will be looked up on the parent map.
* <br/>The raison d'etre of this class is to the {@link #keySet()} method. This methods returns a set of <strong>all</strong> the keys contained in the child and the parent.
* That's the main reason to not used the {@link java.util.Properties} class (which offers access to child keys only).
* <p/>The child has an immutable view of the parent map. Methods {@link #clear()} and {@link #remove(Object)}
* throw {@link UnsupportedOperationException}. Methods {@link #put(Object, Object)} and {@link #putAll(java.util.Map)} impacts only the child map.
* <br/>Methods {@link #clearLocally(Object)}
* <p/>
* This class does NOT implement the {@link java.util.Map} interface because methods {@link java.util.Map#entrySet()} },
* {@link java.util.Map#values()} and {@link java.util.Map#keySet()} would NOT be backed by the Map itself.
* <br/> Contributions welcome to implement that part.
*
* @author <a href="mailto:midon@intalio.com">Alexis Midon</a>
*/
private static class ChainedMap {
private ChainedMap parent;
private Map child;
public ChainedMap() {
parent = null;
child = new HashMap();
}
public ChainedMap(ChainedMap parent) {
this.parent = parent;
this.child = new HashMap();
}
public ChainedMap getParent() {
return parent;
}
public void setParent(ChainedMap parent) {
this.parent = parent;
}
/**
* Perfom a look up on the child map only.
*/
public Object getLocally(Object key) {
return child.get(key);
}
/**
* Clear the child map only, the parent map is not altered.
*/
public void clearLocally() {
child.clear();
}
/**
* Perform a look up for the given key on the child map, and if not found then perform the look up on the parent map.
*
* @param key
* @return
*/
public Object get(Object key) {
Object lv = getLocally(key);
if (lv != null) return lv;
else if (parent != null) return parent.get(key);
return null;
}
/**
* Put this pair in the child map.
*/
public Object put(Object key, Object value) {
if (key == null) throw new NullPointerException("Null keys forbidden!");
return child.put(key, value);
}
/**
* Put these pairs in the child map.
*/
public void putAll(Map t) {
for (Object e : t.entrySet()) {
put(((Map.Entry) e).getKey(), ((Map.Entry) e).getValue());
}
}
/**
* @throws UnsupportedOperationException
* @see #clearLocally()
*/
public void clear() {
throw new UnsupportedOperationException();
}
/**
* @throws UnsupportedOperationException
*/
public Object remove(Object key) {
throw new UnsupportedOperationException();
}
/**
* @return true if the child map is empty AND the parent map is null or empty as well.
* <pre>child.isEmpty() && (parent == null || parent.isEmpty());</pre>
*/
public boolean isEmpty() {
return child.isEmpty() && (parent == null || parent.isEmpty());
}
/**
* @return true if the child map contains this key OR the parent map is not null and contains this key.
* <pre>child.containsKey(key) || (parent != null && parent.containsKey(key));</pre>
*/
public boolean containsKey(Object key) {
if (key == null) throw new NullPointerException("Null keys forbidden!");
return child.containsKey(key) || (parent != null && parent.containsKey(key));
}
/**
* @return true if the child map contains this value OR the parent is not null
* <pre>child.containsValue(value) || (parent != null && parent.containsValue(value));</pre>
*/
public boolean containsValue(Object value) {
return child.containsValue(value) || (parent != null && parent.containsValue(value));
}
public int size() {
return keySet().size();
}
/**
* @return a new set instance merging all keys contained in the child and parent maps. <strong>The returned set is not backed by the maps.</strong>
* Any references to the returned sets are hold at the holder's own risks. This breaks the general {@link java.util.Map#entrySet()} contract.
*/
public Set keySet() {
HashSet s = new HashSet(child.keySet());
if (parent != null) s.addAll(parent.keySet());
return s;
}
}
}