package osgi.enroute.configurer.extender;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.Constants;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.coordinator.Coordination;
import org.osgi.service.coordinator.Coordinator;
import org.osgi.util.tracker.BundleTracker;
import osgi.enroute.configurer.api.ConfigurationDone;
import osgi.enroute.logging.messages.api.Format;
import osgi.enroute.logging.messages.api.LogBook;
import aQute.bnd.annotation.component.Activate;
import aQute.bnd.annotation.component.Component;
import aQute.bnd.annotation.component.Reference;
import aQute.lib.collections.ExtList;
import aQute.lib.converter.Converter;
import aQute.lib.converter.TypeReference;
import aQute.lib.io.IO;
import aQute.lib.json.JSONCodec;
import aQute.lib.properties.PropertiesParser;
import aQute.lib.settings.Settings;
import aQute.libg.sed.Domain;
import aQute.libg.sed.ReplacerAdapter;
/**
* This bundle is an extender that reads the bundle's configuration directory.
* Any resources in this directory that and in .json files are treated as
* configurations.
*/
@Component(provide = {
ConfigurationDone.class, Object.class
}, immediate = true)
public class Configurer implements ConfigurationDone, Domain {
private static final String SERVICE_FACTORY_PID = "service.factoryPid";
private static final String OSGI_ENROUTE_CONFIGURER_PID = "osgi.enroute.configurer.pid";
static final JSONCodec codec = new JSONCodec();
static final Converter converter = new Converter();
static Pattern PROFILE_PATTERN = Pattern.compile("\\[([a-zA-Z0-9]+)\\](.*)");
static Pattern RESOURCE_PATTERN = Pattern.compile("(@\\{resource:([^}]+)\\})");
static Pattern MACRO = Pattern.compile("\\$\\{[^}]+\\}");
final List<String> properties = new ArrayList<String>();
final Settings settings = new Settings("~/.enroute/settings.json");
BundleTracker< ? > tracker;
ConfigurationAdmin cm;
String profile;
File dir;
Map<String,String> basicProperties;
interface EnRouteConfigurer extends LogBook {
@Format("Failed to set the configuration %3$s for %1$s: %2$s")
ERROR failedToSetConfigurationFor(Bundle bundle, Exception e, URL url);
@Format("Found a configuration without a pid: %s")
ERROR configurationWithoutPid(Map<String,Object> map);
@Format("Unexpected error while parsing bundle %s: %s")
ERROR parsingBundleFailed(Bundle bundle, Exception e);
@Format("Update configuration from bundle %s for factory=%s, pid=%s")
INFO updatedConfiguration(Bundle bundle, String factory, String pid);
@Format("Log message from bundle %s, pid %s: %s")
INFO logMessage(Bundle bundle, String pid, String message);
WARN unresolvedMacro(String key, Object value);
DEBUG delta(Object object, String key, Object value, Object value2);
@Format("Property files were specified from the command line but no file could be found: %s")
ERROR missingProperties(File f);
}
EnRouteConfigurer log;
Coordinator coordinator;
/*
* Activate this extender and start looking for bundles
*/
@Activate
void activate(BundleContext context) throws Exception {
dir = context.getDataFile("resources");
dir.mkdirs();
//
// Collect properties about our context
// so the configuration can use it
//
basicProperties = new HashMap<String,String>();
basicProperties.putAll(toMap(System.getProperties()));
basicProperties.putAll(settings);
for (String path : properties) {
File f = IO.getFile(path);
if (!f.isFile()) {
log.missingProperties(f);
} else {
Properties props = PropertiesParser.parse(f.toURI());
basicProperties.putAll(toMap(props));
}
}
if (profile == null)
profile = basicProperties.containsKey("profile") ? basicProperties.get("profile") : "debug";
tracker = new BundleTracker<Object>(context, Bundle.ACTIVE | Bundle.STARTING, null) {
@Override
public Object addingBundle(Bundle bundle, BundleEvent event) {
Coordination c = coordinator.begin("enroute::configurer", 0);
try {
Domain domain = Configurer.this;
URL vars = bundle.getEntry("vars.properties");
if (vars != null) {
Properties props = PropertiesParser.parse(vars.toURI());
domain = new MapDomain(domain, toMap(props));
}
Map<String,String> bprops = new HashMap<String,String>();
bprops.put("osgi.bundle.id", bundle.getBundleId() + "");
bprops.put("osgi.bundle.location", bundle.getLocation());
bprops.put("osgi.bundle.bsn", bundle.getSymbolicName());
bprops.put("osgi.bundle.version", bundle.getVersion() + "");
bprops.put("osgi.bundle.lastmodified", bundle.getLastModified() + "");
bprops.put("osgi.current.time", System.currentTimeMillis() + "");
domain = new MapDomain(domain, bprops);
Enumeration<URL> entries = bundle.findEntries("configuration", "*.json", false);
while (entries.hasMoreElements()) {
URL url = entries.nextElement();
try {
String s = IO.collect(url);
configure(bundle, s, domain);
}
catch (Exception e) {
log.failedToSetConfigurationFor(bundle, e, url);
}
}
}
catch (Exception e) {
c.fail(e);
log.parsingBundleFailed(bundle, e);
}
finally {
c.end();
}
return null;
}
};
tracker.open();
//
// We also support local configurations that are set as System
// properties
//
String s = System.getProperty("enroute.configuration");
if (s != null) {
Coordination c = coordinator.begin("enroute::configurer-system", 0);
try {
configure(context.getBundle(), s, this);
}
catch (Throwable e) {
c.fail(e);
}
finally {
c.end();
}
}
}
@SuppressWarnings({
"rawtypes", "unchecked"
})
private Map<String,String> toMap(Map props) {
return props;
}
/*
* Just close the tracker on deactivate
*/
void deactivate() {
tracker.close();
}
/*
* This function takes the configuration data strings, replaces all macros,
* gets the records, and for each record processes it deeper.
*/
void configure(Bundle ctx, String s, Domain domain) throws Exception {
ReplacerAdapter ra = new ReplacerAdapter(domain) {
@SuppressWarnings("unused")
public String _resource(String args[]) {
return null;
// TODO
}
};
//
// In bnd we use ${} macros as well. Sometimes it is nice
// to have both macro processors. So we replace all @{ to
// to ${ since the @{ will not be replaced by bnd.
//
s = s.replaceAll("(?!\\\\)@\\{", "${");
//
// Preprocess the source configuration. Note that we need
// to do this on source level since the JSON will consist
// of real objects
//
String processed = ra.process(s);
//
// Convert the input to a list of maps.
//
List<Hashtable<String,Object>> list = codec.dec().from(processed)
.get(new TypeReference<List<Hashtable<String,Object>>>() {});
for (Hashtable<String,Object> map : list)
configure(ctx, map);
}
/*
* We have a configuration record for this bundle. We clean up the record.
*/
void configure(Bundle bundle, Map<String,Object> map) throws Exception {
//
// Check if this is a valid configuration
//
String factory = (String) map.get(SERVICE_FACTORY_PID);
String pid = (String) map.get(Constants.SERVICE_PID);
if (pid == null) {
log.configurationWithoutPid(map);
return;
}
//
// Need to clean this up, and we have a couple of comments
// and log messages
//
Hashtable<String,Object> dictionary = fixup(bundle, pid, map);
//
// We need to handle symbolic pids when factories are used since
// the PIDs are assigned by the CM. We need to remember which
// symbolic PID we used for an instance so that we can update
// that specific factory instance when we get an update. To
// address this, we store an extra property in the dict.
//
dictionary.put(OSGI_ENROUTE_CONFIGURER_PID, pid);
Configuration configuration;
if (factory != null) {
//
// If we have a factory then we need to find out if there
// is already a record available. We need to search for
// our symbolic name
//
Configuration instances[] = cm.listConfigurations("(" + OSGI_ENROUTE_CONFIGURER_PID + "=" + pid + ")");
if (instances == null) {
//
// New factory configuration. Make sure it has multiple
// locations
//
configuration = cm.createFactoryConfiguration(factory, "?");
} else {
//
// Existing factory configuration
//
configuration = instances[0];
}
} else {
//
// normal target configuration, will be created
//
configuration = cm.getConfiguration(pid, "?");
}
// System.out.println("Updating " + dictionary);
configuration.setBundleLocation(null);
Dictionary< ? , ? > current = configuration.getProperties();
if (current != null && isEqual(dictionary, current))
return;
configuration.update(dictionary);
log.updatedConfiguration(bundle, factory, pid);
}
Hashtable<String,Object> fixup(Bundle bundle, String pid, Map<String,Object> map) throws Exception {
Hashtable<String,Object> dictionary = new Hashtable<String,Object>();
for (Entry<String,Object> e : map.entrySet()) {
Matcher m = PROFILE_PATTERN.matcher(e.getKey());
if (m.matches()) {
//
// Profile prefixed variables are ignored when they
// are not current (not the profile set right now)
// and otherwise the profile prefix is removed
//
String profile = m.group(1);
if (profile.equals(this.profile))
dictionary.put(m.group(2), e.getValue());
} else if (e.getKey().equals(".log")) {
//
// We allow a record to define a log message
//
log.logMessage(bundle, pid, converter.convert(String.class, e.getValue()));
} else if (e.getKey().equals(".comment")) {
//
// Provides space for comments
// so we better ignore them
//
} else {
Object value = e.getValue();
if (value != null && value instanceof String) {
Matcher matcher = MACRO.matcher((String) value);
while (matcher.find()) {
log.unresolvedMacro(e.getKey(), value);
}
}
dictionary.put(e.getKey(), e.getValue());
}
}
return dictionary;
}
@SuppressWarnings("unchecked")
private boolean isEqual(Hashtable<String,Object> a, Dictionary< ? , ? > b) {
for (Entry<String,Object> e : a.entrySet()) {
if (e.getKey().equals("service.pid"))
continue;
Object value = b.get(e.getKey());
if (value == e.getValue())
continue;
if (value == null)
return false;
if (e.getValue() == null)
return false;
if (value.equals(e.getValue()))
continue;
if (value.getClass().isArray()) {
Object[] aa = {
value
};
Object[] bb = {
e.getValue()
};
if (!Arrays.deepEquals(aa, bb))
return false;
} else if (value instanceof Collection && e.getValue() instanceof Collection) {
ExtList<Object> aa = new ExtList<Object>((Collection<Object>) value);
ExtList<Object> bb = new ExtList<Object>((Collection<Object>) e.getValue());
if (!aa.equals(bb))
return false;
} else {
log.delta(a.get("service.pid"), e.getKey(), value, e.getValue());
return false;
}
}
return true;
}
@Override
public Map<String,String> getMap() {
return basicProperties;
}
@Override
public Domain getParent() {
return null;
}
@Reference
void setLogService(LogBook log) {
this.log = log.scoped(EnRouteConfigurer.class, "enroute::configurer");
}
@Reference
void setCM(ConfigurationAdmin cm) {
this.cm = cm;
}
@Reference
void setCoordinator(Coordinator coordinator) {
this.coordinator = coordinator;
}
@Reference(type = '?', target = "(launcher.arguments=*)")
synchronized void setLauncher(Object obj, Map<String,Object> props) {
String[] args = (String[]) props.get("launcher.arguments");
for (int i = 0; i < args.length - 1; i++) {
if (args[i].equals("--enroute-profile")) {
if (this.profile != null)
System.err.println("Profile set multiple times, used first one " + this.profile);
else
this.profile = args[i++];
} else if (args[i].equals("--enroute-properties")) {
this.properties.add(args[++i]);
}
}
}
synchronized void unsetLauncher(Object obj) {
}
}