blob: 8c0b27cdea528785e61339f90167c9b884f9026c [file] [log] [blame]
/*
* 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.felix.utils.properties;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.apache.felix.utils.properties.InterpolationHelper.substVars;
/**
* <p>
* Map to load / store / update untyped or typed properties.
* The map is untyped if all properties are strings.
* When this is the case, the properties are stored without
* any encoding, else all properties are encoded using
* the {@link ConfigurationHandler}.
* </p>
*
* @author gnodet
*/
public class TypedProperties extends AbstractMap<String, Object> {
public static final String ENV_PREFIX = "env:";
private final Properties storage;
private final SubstitutionCallback callback;
private final boolean substitute;
public TypedProperties() {
this(null, true);
}
public TypedProperties(boolean substitute) {
this(null, substitute);
}
public TypedProperties(SubstitutionCallback callback) {
this(callback, true);
}
public TypedProperties(SubstitutionCallback callback, boolean substitute) {
this.storage = new Properties(false);
this.callback = callback;
this.substitute = substitute;
}
public void load(File location) throws IOException {
InputStream is = new FileInputStream(location);
try {
load(is);
} finally {
is.close();
}
}
public void load(URL location) throws IOException {
InputStream is = location.openStream();
try {
load(is);
} finally {
is.close();
}
}
public void load(InputStream is) throws IOException {
load(new InputStreamReader(is, Properties.DEFAULT_ENCODING));
}
public void load(Reader reader) throws IOException {
storage.loadLayout(reader, true);
substitute(callback);
}
public void save(File location) throws IOException {
storage.save(location);
}
public void save(OutputStream os) throws IOException {
storage.save(os);
}
public void save(Writer writer) throws IOException {
storage.save(writer);
}
/**
* Store a properties into a output stream, preserving comments, special character, etc.
* This method is mainly to be compatible with the java.util.Properties class.
*
* @param os an output stream.
* @param comment this parameter is ignored as this Properties
* @throws IOException If storing fails
*/
public void store(OutputStream os, String comment) throws IOException {
storage.store(os, comment);
}
@Override
public Set<Entry<String, Object>> entrySet() {
return new AbstractSet<Entry<String, Object>>() {
@Override
public Iterator<Entry<String, Object>> iterator() {
return new Iterator<Entry<String, Object>>() {
final Iterator<String> keyIterator = storage.keySet().iterator();
public boolean hasNext() {
return keyIterator.hasNext();
}
public Entry<String, Object> next() {
final String key = keyIterator.next();
return new Entry<String, Object>() {
public String getKey() {
return key;
}
public Object getValue() {
return TypedProperties.this.get(key);
}
public Object setValue(Object value) {
return TypedProperties.this.put(key, value);
}
};
}
public void remove() {
keyIterator.remove();
}
};
}
@Override
public int size() {
return storage.size();
}
};
}
@Override
public Object put(String key, Object value) {
if (value instanceof String && !storage.typed) {
return storage.put(key, (String) value);
} else {
ensureTyped();
String old = storage.put(key, convertToString(value));
return old != null ? convertFromString(old) : null;
}
}
@Override
public Object get(Object key) {
String v = storage.get(key);
return storage.typed && v != null ? convertFromString(v) : v;
}
public Object put(String key, List<String> commentLines, Object value) {
if (value instanceof String && !storage.typed) {
return storage.put(key, commentLines, (String) value);
} else {
ensureTyped();
return put(key, commentLines, Arrays.asList(convertToString(value).split("\n")));
}
}
public Object put(String key, String comment, Object value) {
return put(key, Collections.singletonList(comment), value);
}
public Object put(String key, List<String> commentLines, List<String> valueLines) {
String old = storage.put(key, commentLines, valueLines);
return old != null ? storage.typed ? convertFromString(old) : old : null;
}
private void ensureTyped() {
if (!storage.typed) {
storage.typed = true;
Set<String> keys = new HashSet<String>(storage.keySet());
for (String key : keys) {
storage.put(key,
storage.getComments(key),
Arrays.asList(convertToString(storage.get(key)).split("\n")));
}
}
}
public boolean update(Map<String, Object> props) {
TypedProperties properties;
if (props instanceof TypedProperties) {
properties = (TypedProperties) props;
} else {
properties = new TypedProperties();
for (Entry<String, Object> e : props.entrySet()) {
properties.put(e.getKey(), e.getValue());
}
}
return update(properties);
}
public boolean update(TypedProperties properties) {
return storage.update(properties.storage);
}
public List<String> getRaw(String key) {
return storage.getRaw(key);
}
public List<String> getComments(String key) {
return storage.getComments(key);
}
@Override
public Object remove(Object key) {
return storage.remove(key);
}
@Override
public void clear() {
storage.clear();
}
/**
* Return the comment header.
*
* @return the comment header
*/
public List<String> getHeader()
{
return storage.getHeader();
}
/**
* Set the comment header.
*
* @param header the header to use
*/
public void setHeader(List<String> header)
{
storage.setHeader(header);
}
/**
* Return the comment footer.
*
* @return the comment footer
*/
public List<String> getFooter()
{
return storage.getFooter();
}
/**
* Set the comment footer.
*
* @param footer the footer to use
*/
public void setFooter(List<String> footer)
{
storage.setFooter(footer);
}
public void substitute(final SubstitutionCallback cb) {
if (!substitute) {
return;
}
final SubstitutionCallback callback = cb != null ? cb : new SubstitutionCallback() {
public String getValue(String name, String key, String value) {
if (value.startsWith(ENV_PREFIX))
{
return System.getenv(value.substring(ENV_PREFIX.length()));
}
else
{
return System.getProperty(value);
}
}
}; //wrap(new BundleContextSubstitutionCallback(null));
Map<String, TypedProperties> props = Collections.singletonMap("root", this);
substitute(props, prepare(props), callback, true);
}
private static SubstitutionCallback wrap(final InterpolationHelper.SubstitutionCallback cb) {
return new SubstitutionCallback() {
public String getValue(String name, String key, String value) {
return cb.getValue(value);
}
};
}
public interface SubstitutionCallback {
String getValue(String name, String key, String value);
}
public static Map<String, Map<String, String>> prepare(Map<String, TypedProperties> properties) {
Map<String, Map<String, String>> dynamic = new HashMap<String, Map<String, String>>();
for (Map.Entry<String, TypedProperties> entry : properties.entrySet()) {
String name = entry.getKey();
dynamic.put(name, new DynamicMap(name, entry.getValue().storage));
}
return dynamic;
}
public static void substitute(Map<String, TypedProperties> properties,
Map<String, Map<String, String>> dynamic,
SubstitutionCallback callback,
boolean finalSubstitution) {
for (Map<String, String> map : dynamic.values()) {
((DynamicMap) map).init(callback, finalSubstitution);
}
for (Map.Entry<String, TypedProperties> entry : properties.entrySet()) {
entry.getValue().storage.putAllSubstituted(dynamic.get(entry.getKey()));
}
}
private static String convertToString(Object value) {
try {
return ConfigurationHandler.write(value);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static Object convertFromString(String value) {
try {
return ConfigurationHandler.read(value);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static class DynamicMap extends AbstractMap<String, String> {
private final String name;
private final Properties storage;
private final Map<String, String> computed;
private final Map<String, String> cycles;
private SubstitutionCallback callback;
private boolean finalSubstitution;
public DynamicMap(String name, Properties storage) {
this.name = name;
this.storage = storage;
this.computed = new HashMap<String, String>();
this.cycles = new HashMap<String, String>();
}
public void init(SubstitutionCallback callback, boolean finalSubstitution) {
this.callback = callback;
this.finalSubstitution = finalSubstitution;
}
@Override
public Set<Entry<String, String>> entrySet() {
return new AbstractSet<Entry<String, String>>() {
@Override
public Iterator<Entry<String, String>> iterator() {
final Iterator<String> iterator = storage.keySet().iterator();
return new Iterator<Entry<String, String>>() {
public boolean hasNext() {
return iterator.hasNext();
}
public Entry<String, String> next() {
final String key = iterator.next();
return new Entry<String, String>() {
public String getKey() {
return key;
}
public String getValue() {
String v = computed.get(key);
if (v == null) {
v = compute(key);
// computed.put(key, v);
}
return v;
}
public String setValue(String value) {
throw new UnsupportedOperationException();
}
};
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
public int size() {
return storage.size();
}
};
}
private String compute(final String key) {
InterpolationHelper.SubstitutionCallback wrapper = new InterpolationHelper.SubstitutionCallback() {
public String getValue(String value) {
if (finalSubstitution) {
String str = DynamicMap.this.get(value);
if (str != null) {
if (storage.typed) {
boolean mult;
boolean hasType;
char t = str.charAt(0);
if (t == '[' || t == '(') {
mult = true;
hasType = false;
} else if (t == '"') {
mult = false;
hasType = false;
} else {
t = str.charAt(1);
mult = t == '[' || t == '(';
hasType = true;
}
if (mult) {
throw new IllegalArgumentException("Can't substitute from a collection/array value: " + value);
}
return (String) convertFromString(hasType ? str.substring(1) : str);
} else {
return str;
}
}
}
return callback.getValue(name, key, value);
}
};
String value = storage.get(key);
String v = substVars(value, key, cycles, this, wrapper, false, finalSubstitution, finalSubstitution);
return v;
}
}
}