| /* |
| * 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.collections.*; |
| 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.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * This class load a list of regular property files (order matters). 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> |
| *alias.foo_ns=http://foo.com |
| * |
| * timeout=40000 |
| * a_namespace_with_no_alias_defined.film-service.port-of-cannes.ode.timeout=50000 |
| * <p/> |
| * max-redirects=30 |
| * foo_ns.brel-service.ode.max-redirects=40 |
| * foo_ns.brel-service.port-of-amsterdam.ode.max-redirects=60 |
| * </pre> |
| * The following values may be expected: |
| * <pre> |
| * getProperty("max-redirects") => 30 |
| * getProperty("http://foo.com", "brel-service", "max-redirects") => 40 |
| * getProperty("http://foo.com", "brel-service", "port-of-amsterdam", "max-redirects") => 60 |
| * <p/> |
| * getProperty("a_namespace_with_no_alias_defined", "film-service", "timeout") => 40000 |
| * getProperty("a_namespace_with_no_alias_defined", "film-service", "port-of-cannes", "timeout") => 50000 |
| * getProperty("http://foo.com", "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"; |
| |
| private File[] files; |
| private String prefix; |
| private String dotted_prefix; |
| /* |
| This map contains ChainedMap instances chained according to the (qualified) 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 files 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. Files will be loaded in the given order. |
| * @param prefix the property prefix |
| * @throws IOException |
| */ |
| public HierarchicalProperties(File[] files, String prefix) throws IOException { |
| this.files = files; |
| this.prefix = prefix; |
| this.dotted_prefix = "." + prefix + "."; |
| loadFiles(); |
| } |
| |
| public HierarchicalProperties(File[] files) throws IOException { |
| this(files, ODE_PREFFIX); |
| } |
| |
| public HierarchicalProperties(File file, String prefix) throws IOException { |
| this(new File[]{file}, prefix); |
| } |
| |
| public HierarchicalProperties(File file) throws IOException { |
| this(new File[]{file}, ODE_PREFFIX); |
| } |
| |
| public HierarchicalProperties(List<File> propFiles) throws IOException { |
| this(propFiles.toArray(new File[propFiles.size()]), ODE_PREFFIX); |
| } |
| |
| /** |
| * Clear all existing content, then 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 loadFiles() throws IOException { |
| // #1. clear all existing content |
| clear(); |
| // #3. put the root map |
| hierarchicalMap.put(null, null, new ChainedMap()); |
| |
| for (File file : files) loadFile(file); |
| } |
| |
| public void loadFile(File file) throws IOException { |
| if (!file.exists()) { |
| if (log.isDebugEnabled()) log.debug("File does not exist [" + file + "]"); |
| return; |
| } |
| Properties props = new Properties(); |
| // #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(); |
| } |
| |
| // gather all aliases |
| Map<String, String> nsByAlias = new HashMap<String, String>(); |
| |
| for (Iterator it = props.entrySet().iterator(); it.hasNext();) { |
| Map.Entry e = (Map.Entry) it.next(); |
| String key = (String) e.getKey(); |
| String namespace = (String) e.getValue(); |
| |
| |
| // replace any env variables by its value |
| namespace = SystemUtils.replaceSystemProperties(namespace); |
| props.put(key, namespace); |
| |
| if (key.startsWith("alias.")) { |
| final String alias = key.substring("alias.".length(), key.length()); |
| if (log.isDebugEnabled()) log.debug("Alias found: " + alias + " -> " + namespace); |
| if (nsByAlias.containsKey(alias) && namespace.equals(nsByAlias.get(alias))) |
| throw new RuntimeException("Same alias used twice for 2 different namespaces! file=" + file + ", alias=" + alias); |
| nsByAlias.put(alias, namespace); |
| // remove the pair |
| it.remove(); |
| } |
| } |
| |
| // #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(); |
| |
| |
| // parse the property name |
| String[] info = parseProperty(key); |
| String nsalias = info[0]; |
| String service = info[1]; |
| String port = info[2]; |
| String targetedProperty = info[3]; |
| |
| QName qname = null; |
| if (nsalias != null) { |
| qname = new QName(nsByAlias.get(nsalias) != null ? nsByAlias.get(nsalias) : nsalias, service); |
| } |
| // 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 the service map |
| p = new ChainedMap(s); |
| // put it in the multi-map |
| hierarchicalMap.put(qname, port, p); |
| } |
| |
| // save the key/value in its chained map |
| if(log.isDebugEnabled()) log.debug("New property: "+targetedProperty+" -> "+value); |
| p.put(targetedProperty, value); |
| } |
| } |
| |
| /** |
| * Clear all content. If {@link #loadFiles()} is not invoked later, all returned values will be null. |
| */ |
| public void 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; |
| |
| // 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) { |
| return (String) getProperties(service, port).get(property); |
| } |
| |
| public String getPrefix() { |
| return prefix; |
| } |
| |
| |
| 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; |
| } |
| } |
| } |