blob: 18639ce04a88ec57962bf5eb020adfa11936bac9 [file] [log] [blame]
/**
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.winegrower.service;
import org.apache.winegrower.lang.Substitutor;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationEvent;
import org.osgi.service.cm.ConfigurationListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Date;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import static java.util.Arrays.asList;
import static java.util.Collections.list;
import static java.util.Locale.ROOT;
import static java.util.Optional.ofNullable;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;
public abstract class DefaultConfigurationAdmin implements ConfigurationAdmin {
private final static String WINEGROWER_CONFIG_PATH = "winegrower.config.path";
private final static String WINEGROWER_CONFIG_EXTENSION = ".cfg";
private final Map<String, String> providedConfiguration;
private final Map<Key, Configuration> configurations = new HashMap<>();
private final Collection<ConfigurationListener> configurationListeners;
public DefaultConfigurationAdmin(final Map<String, String> providedConfiguration,
final Collection<ConfigurationListener> configurationListeners) {
this.providedConfiguration = providedConfiguration;
this.configurationListeners = configurationListeners;
}
public void preload(final List<String> names) {
if (names == null || names.isEmpty()) {
return;
}
names.forEach(it -> getConfiguration(it).setBundleLocation(null));
}
public Map<String, String> getProvidedConfiguration() {
return providedConfiguration;
}
@Override
public Configuration getFactoryConfiguration(final String factoryPid, final String name, final String location) {
return getOrCreate(factoryPid, null, location, name);
}
@Override
public Configuration getFactoryConfiguration(final String factoryPid, final String name) {
return getOrCreate(factoryPid, null, null, name);
}
@Override
public Configuration createFactoryConfiguration(final String pid) {
return createFactoryConfiguration(pid, null);
}
@Override
public Configuration createFactoryConfiguration(final String pid, final String location) {
return getOrCreate(pid, null, location, null);
}
@Override
public Configuration getConfiguration(final String pid, final String location) {
return getOrCreate(null, pid, location, null);
}
@Override
public Configuration getConfiguration(final String pid) {
return getConfiguration(pid, null);
}
@Override
public Configuration[] listConfigurations(final String filter) {
try {
final Filter predicate = filter == null ? null : FrameworkUtil.createFilter(filter);
return configurations.values().stream().filter(it -> predicate == null || predicate.match(it.getProperties()))
.toArray(Configuration[]::new);
} catch (final InvalidSyntaxException e) {
throw new IllegalArgumentException(e);
}
}
private Configuration getOrCreate(final String factoryPid, final String pid, final String location,
final String name) {
final Key key = new Key(factoryPid, pid);
final Configuration existing = configurations.get(key);
if (existing != null) {
return existing;
}
final DefaultConfiguration created = new DefaultConfiguration(providedConfiguration,
factoryPid, pid, location, name) {
@Override
public void setBundleLocation(final String location) {
super.setBundleLocation(location);
final ConfigurationEvent event = new ConfigurationEvent(
getSelfReference(), ConfigurationEvent.CM_LOCATION_CHANGED, factoryPid, pid);
configurationListeners.forEach(it -> it.configurationEvent(event));
}
@Override
public void update(Dictionary<String, ?> properties) {
super.update(properties);
final ConfigurationEvent event = new ConfigurationEvent(
getSelfReference(), ConfigurationEvent.CM_UPDATED, factoryPid, pid);
configurationListeners.forEach(it -> it.configurationEvent(event));
}
@Override
public void delete() {
super.delete();
final ConfigurationEvent event = new ConfigurationEvent(
getSelfReference(), ConfigurationEvent.CM_DELETED, factoryPid, pid);
configurationListeners.forEach(it -> it.configurationEvent(event));
}
};
configurations.putIfAbsent(key, created);
return created;
}
protected abstract ServiceReference<ConfigurationAdmin> getSelfReference();
private static class DefaultConfiguration implements Configuration {
private final String factoryPid;
private final String pid;
private final Map<String, String> defaultConfig = new HashMap<>();
private final File defaultExternalConfigLocation;
private final Map<String, String> configRegistry;
private final String name;
private String location;
private final Hashtable<String, Object> properties;
private final AtomicLong changeCount = new AtomicLong();
private final Set<ConfigurationAttribute> attributes = new HashSet<>();
private DefaultConfiguration(final Map<String, String> configRegistry, final String factoryPid, final String pid,
final String location, final String name) {
this.configRegistry = configRegistry;
this.factoryPid = factoryPid;
this.pid = pid;
this.location = location;
this.name = name;
this.properties = new Hashtable<>();
this.defaultExternalConfigLocation = new File(
// support a cascade of known "homes"
System.getProperty(WINEGROWER_CONFIG_PATH,
System.getProperty("karaf.base",
System.getProperty("catalina.base",
System.getProperty("karaf.home", System.getProperty("karaf.etc"))))),
pid + WINEGROWER_CONFIG_EXTENSION);
loadConfig(pid);
}
private void loadConfig(final String pid) {
final String prefix = "winegrower.service." + pid + "."; // for "global" registries like system props
// we first read the config from the classpath (lowest priority)
try (final InputStream embedConfig = Thread.currentThread().getContextClassLoader()
.getResourceAsStream(pid + WINEGROWER_CONFIG_EXTENSION)) {
if (embedConfig != null) {
defaultConfig.putAll(load(embedConfig));
}
} catch (final IOException e) {
throw new IllegalArgumentException(e);
}
properties.putAll(defaultConfig);
// then the default registry which is considered "in JVM" so less prioritized than external config
configRegistry.entrySet().stream().filter(it -> it.getKey().startsWith(prefix))
.forEach(entry -> properties.put(entry.getKey().substring(prefix.length()), entry.getValue()));
// then from an external file
if (defaultExternalConfigLocation.isFile()) {
try (final InputStream stream = new FileInputStream(defaultExternalConfigLocation)) {
this.properties.putAll(load(stream));
} catch (final IOException e) {
throw new IllegalArgumentException(e);
}
}
// and finally from system properties and env variables
// (env is for the machine so less precise than system props so set first)
final String envPrefix = prefix.toUpperCase(ROOT).replace('.', '_');
System.getenv().keySet().stream()
.filter(it -> it.length() > prefix.length() && envPrefix.equalsIgnoreCase(it.substring(0, envPrefix.length())))
.forEach(key -> {
final String k = key.substring(envPrefix.length());
// env keys loose the case so in case it is important, enable to force the key name
// ex: to set configuration{pid=a.b.c, key=fooBar, value=dummy} you would set:
// A_B_C_FOOBAR_NAME=fooBar
// A_B_C_FOOBAR=dummy
// note that the FOOBAR in the key is not important, previous config is the same than:
// A_B_C_1_NAME=fooBar
// A_B_C_1=dummy
// but when there key is not ambiguous (all lowercase) it is simpler to set (key=foobar):
// A_B_C_FOOBAR=dummy
properties.put(
ofNullable(System.getenv(key + "_NAME")).orElseGet(() -> k.toLowerCase(ROOT)),
System.getenv(key));
});
System.getProperties().stringPropertyNames().stream()
.filter(it -> it.startsWith(prefix))
.forEach(key -> properties.put(key.substring(prefix.length()), System.getProperty(key)));
// ensure the factoryPid/pid is there if exists
ofNullable(pid).ifPresent(v -> properties.putIfAbsent("service.pid", v));
ofNullable(factoryPid).ifPresent(v -> properties.putIfAbsent("service.factoryPid", v));
ofNullable(name).ifPresent(v -> properties.putIfAbsent("name", v));
}
@Override
public String getPid() {
return pid;
}
@Override
public Dictionary<String, Object> getProperties() {
return properties;
}
@Override
public Dictionary<String, Object> getProcessedProperties(final ServiceReference<?> reference) {
return reference.getProperties();
}
@Override
public void update(final Dictionary<String, ?> properties) {
this.properties.clear();
loadConfig(pid);
this.properties.putAll(converter(properties));
this.changeCount.incrementAndGet();
}
@Override
public void delete() {
// no-op
}
@Override
public String getFactoryPid() {
return factoryPid;
}
@Override
public void update() {
update(new Hashtable<>());
if (defaultExternalConfigLocation.isFile()) {
final Properties output = new Properties();
output.putAll(properties);
try (final OutputStream outputStream = new FileOutputStream(defaultExternalConfigLocation)) {
output.store(outputStream, "Updated configuration on " + new Date());
} catch (final IOException e) {
throw new IllegalArgumentException(e);
}
} // else: don't modify neither the classpath nor system properties - this would be insane even if doable
this.changeCount.incrementAndGet();
}
@Override
public boolean updateIfDifferent(final Dictionary<String, ?> properties) {
if (properties == null || list(properties.keys()).stream()
.anyMatch(it -> !properties.get(it).equals(this.properties.get(it)))) {
update(properties);
return true;
}
return false;
}
@Override
public void setBundleLocation(final String location) {
this.location = location;
}
@Override
public String getBundleLocation() {
return location;
}
@Override
public long getChangeCount() {
return changeCount.get();
}
@Override
public void addAttributes(final ConfigurationAttribute... attrs) {
ofNullable(attrs).ifPresent(values -> attributes.addAll(asList(attrs)));
}
@Override
public Set<ConfigurationAttribute> getAttributes() {
return attributes;
}
@Override
public void removeAttributes(ConfigurationAttribute... attrs) throws IOException {
ofNullable(attrs).ifPresent(values -> attributes.removeAll(asList(attrs)));
}
private Map<String, String> converter(final Dictionary<String, ?> properties) {
return list(properties.keys()).stream().collect(toMap(identity(), it -> properties.get(it).toString()));
}
private Map<String, String> load(final InputStream stream) {
final Properties properties = new Properties();
try {
properties.load(stream);
} catch (final IOException e) {
throw new IllegalArgumentException(e);
}
final Map<String, String> placeholders = new HashMap<>(Map.class.cast(properties));
placeholders.putAll(Map.class.cast(System.getProperties()));
final Substitutor substitutor = new Substitutor(placeholders);
return properties.stringPropertyNames().stream().collect(toMap(identity(),
it -> it.contains("${") && it.contains("}") ? substitutor.replace(it) : properties.getProperty(it)));
}
}
private static class Key {
private final String factoryPid;
private final String pid;
private final int hash;
private Key(final String factoryPid, final String pid) {
this.factoryPid = factoryPid;
this.pid = pid;
this.hash = Objects.hash(factoryPid, pid);
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final Key key = Key.class.cast(o);
return Objects.equals(factoryPid, key.factoryPid) && Objects.equals(pid, key.pid);
}
@Override
public int hashCode() {
return hash;
}
@Override
public String toString() {
return "Key{factoryPid='" + factoryPid + "', pid='" + pid + "'}";
}
}
}