/*
* Copyright 2010 Red Hat, Inc. and/or its affiliates.
*
* 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 org.kie.internal.utils;
import java.io.Externalizable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Priority
* <ul>
* <li>System properties</li>
* <li>META-INF/ of provided classLoader</li>
* </ul>
* <br/>
* To improve performance in frequent session creation cases, chained properties can be cached by it's conf file name
* and requesting classloader. To take advantage of the case it must be enabled via system property:<br/>
* <code>org.kie.property.cache.enabled</code> that needs to be set to <code>true</code>
* Cache entries are by default limited to 100 to reduce memory consumption but can be fine tuned by system property:<br/>
* <code>org.kie.property.cache.size</code> that needs to be set to valid integer value
*/
public class ChainedProperties
implements
Externalizable {
protected static transient Logger logger = LoggerFactory.getLogger(ChainedProperties.class);
private static final int MAX_CACHE_ENTRIES = 100;
protected static Map<CacheKey, ChainedProperties> propertiesCache =
Collections.synchronizedMap(
new LinkedHashMap<CacheKey, ChainedProperties>() {
private static final long serialVersionUID = -4728876927433598466L;
protected boolean removeEldestEntry(
Map.Entry<CacheKey, ChainedProperties> eldest) {
return size() > MAX_CACHE_ENTRIES;
}
});
private List<Properties> props = new ArrayList<Properties>();
private List<Properties> defaultProps = new ArrayList<Properties>();
public static ChainedProperties getChainedProperties( ClassLoader classLoader ) {
return getChainedProperties( "properties.conf", classLoader );
}
public static ChainedProperties getChainedProperties( String confFileName, ClassLoader classLoader ) {
CacheKey key = new CacheKey( confFileName, classLoader );
ChainedProperties chainedProperties = propertiesCache.get(key);
if (chainedProperties == null) {
chainedProperties = new ChainedProperties( confFileName, classLoader );
propertiesCache.put(key, chainedProperties);
}
ChainedProperties props = chainedProperties.clone();
props.addProperties( System.getProperties() );
return props;
}
protected ChainedProperties clone() {
ChainedProperties clone = new ChainedProperties();
clone.props.addAll( this.props );
clone.defaultProps.addAll( this.defaultProps );
return clone;
}
public ChainedProperties() {
}
private ChainedProperties(String confFileName, ClassLoader classLoader) {
loadProperties( "META-INF/kie." + confFileName, classLoader, this.props );
loadProperties( "META-INF/kie.default." + confFileName, classLoader, this.defaultProps);
// this happens only in OSGi: for some reason doing
// ClassLoader.getResources() doesn't work but doing
// Class.getResourse() does
if (this.defaultProps.isEmpty()) {
try {
Class<?> c = Class.forName( "org.drools.core.WorkingMemory", false, classLoader);
URL confURL = c.getResource("/META-INF/kie.default." + confFileName);
loadProperties(confURL, this.defaultProps);
} catch (ClassNotFoundException e) { }
}
}
@SuppressWarnings("unchecked")
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
props = (List<Properties>) in.readObject();
defaultProps = (List<Properties>) in.readObject();
}
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject( props );
out.writeObject( defaultProps );
}
/**
* Specifically added properties take priority, so they go to the front of the list.
* @param properties
*/
public void addProperties(Properties properties) {
this.props.add( 0, properties );
}
public String getProperty(String key,
String defaultValue) {
String value = null;
for ( Properties props : this.props ) {
value = props.getProperty( key );
if ( value != null ) {
break;
}
}
if ( value == null ) {
for ( Properties props : this.defaultProps ) {
value = props.getProperty( key );
if ( value != null ) {
break;
}
}
}
return (value != null) ? value : defaultValue;
}
public void mapStartsWith(Map<String, String> map,
String startsWith,
boolean includeSubProperties) {
for ( Properties props : this.props ) {
mapStartsWith( map,
props,
startsWith,
includeSubProperties );
}
for ( Properties props : this.defaultProps ) {
mapStartsWith( map,
props,
startsWith,
includeSubProperties );
}
}
private void mapStartsWith(Map<String, String> map,
Properties properties,
String startsWith,
boolean includeSubProperties) {
Enumeration< ? > enumeration = properties.propertyNames();
while ( enumeration.hasMoreElements() ) {
String key = (String) enumeration.nextElement();
if ( key.startsWith( startsWith ) ) {
if ( !includeSubProperties && key.substring( startsWith.length() + 1 ).indexOf( '.' ) > 0 ) {
// +1 to the length, as we do allow the direct property, just not ones below it
// This key has sub properties beyond the given startsWith, so skip
continue;
}
if ( !map.containsKey( key ) ) {
map.put( key,
properties.getProperty( key ) );
}
}
}
}
private void loadProperties(String fileName,
List<Properties> chain) {
if (fileName != null) {
File file = new File(fileName);
if (file != null && file.exists()) {
loadProperties(fileName, null, chain);
}
}
}
private void loadProperties(String fileName,
ClassLoader classLoader,
List<Properties> chain) {
try {
chain.addAll(read(fileName,classLoader));
} catch (IOException e){}
}
private List<Properties> read(String fileName, ClassLoader classLoader)
throws IOException {
List<Properties> properties = new ArrayList<>();
Enumeration<URL> resources;
if (classLoader != null) {
resources = classLoader.getResources(fileName);
} else {
resources = Collections.enumeration(Collections.singletonList(new File(fileName).toURI().toURL()));
}
while (resources.hasMoreElements()) {
Properties p = new Properties();
URL nextElement = resources.nextElement();
try (InputStream is = nextElement.openStream()) {
p.load(is);
properties.add(p);
}
}
return properties;
}
private void loadProperties(URL confURL, List<Properties> chain) {
if ( confURL == null ) {
return;
}
try ( InputStream is = confURL.openStream() ) {
Properties properties = new Properties();
properties.load( is );
chain.add( properties );
} catch ( IOException e ) {
//throw new IllegalArgumentException( "Invalid URL to properties file '" + confURL.toExternalForm() + "'" );
}
}
/*
* optional cache handling to improve performance to avoid duplicated loads of properties
*/
private static class CacheKey {
private String confFileName;
private ClassLoader classLoader;
CacheKey(String confFileName, ClassLoader classLoader) {
this.confFileName = confFileName;
this.classLoader = classLoader;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((classLoader == null) ? 0 : classLoader.hashCode());
result = prime * result
+ ((confFileName == null) ? 0 : confFileName.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
CacheKey other = (CacheKey) obj;
if (classLoader == null) {
if (other.classLoader != null) {
return false;
}
} else if (!classLoader.equals(other.classLoader)) {
return false;
}
if (confFileName == null) {
if (other.confFileName != null) {
return false;
}
} else if (!confFileName.equals(other.confFileName)) {
return false;
}
return true;
}
@Override
public String toString() {
return "CacheKey [confFileName=" + confFileName + ", classLoader="
+ classLoader + "]";
}
}
}