package osgi.configurer.provider; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; 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.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.coordinator.Coordination; import org.osgi.service.coordinator.Coordinator; import org.osgi.service.log.LogService; import org.osgi.util.tracker.BundleTracker; import osgi.enroute.configurer.api.ConfigurationDone; import osgi.enroute.configurer.api.ProvideConfigurerExtender; 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.settings.Settings; import aQute.libg.sed.ReplacerAdapter; /** * This component is an extender that reads {@link #CONFIGURATION_LOC} file. * These files are JSON formatted, though they do support comments. * Additionally, if the enRoute.configurer System property is set, it is also * read. * <p> * This configurer also supports profiles. The default profile is "debug", * profiles can be set with the --profile option in the command line (if an * appropriate launcher is used), or setting the enRoute.configurer.profile * System property is set. * <p> * Macros are fully supported. The variable order is configuration, system * properties, settings in ~/.enRoute/settings.json. * <p> * The configurer can also refer to binary resources with @{resource-path}. This * pattern requires a resource path inside the bundle. This resource is copied * to the local file system and the macro is replaced with the corresponding * path. * <p> * The configurer reads */ @ProvideConfigurerExtender @Component(service = { ConfigurationDone.class, Object.class }, immediate = true) public class Configurer implements ConfigurationDone { private static final String LOGICAL_PID_KEY = "._osgi.enroute.pid"; private static final String BUNDLE_KEY = "._osgi.enroute.bundle"; private static final String EN_ROUTE_PROFILE = "enRoute.profile"; private static final String CONFIGURER_EXTRA = "enRoute.configurer.extra"; private static final String CONFIGURATION_LOC = "configuration/configuration.json"; public static final JSONCodec codec = new JSONCodec(); public static final Converter converter = new Converter(); static Pattern PROFILE_PATTERN = Pattern.compile("\\[([a-zA-Z0-9]+)\\](.*)"); Settings settings = new Settings("~/.enRoute/settings.json"); LogService log; BundleTracker< ? > tracker; ConfigurationAdmin cm; String profile; File dir; Map<String,String> base; Bundle currentBundle; private Coordinator coordinator; /* * Track all bundles and read their configuration. */ @Activate void activate(BundleContext context) throws Exception { // // Reserve space for the resources // dir = context.getDataFile("resources"); dir.mkdirs(); Map<String,String> map = new HashMap<>(); map.putAll(settings); map.putAll(converter.convert(new TypeReference<Map<String,String>>() {}, System.getProperties())); this.base = Collections.unmodifiableMap(map); Coordination coordination = coordinator.begin("enRoute.configurer", TimeUnit.SECONDS.toMillis(20)); try { tracker = new BundleTracker<Object>(context, Bundle.ACTIVE | Bundle.STARTING, null) { @Override public Object addingBundle(Bundle bundle, BundleEvent event) { try { String h = bundle.getHeaders().get(ConfigurationDone.BUNDLE_CONFIGURATION); if (h == null) // // We use a default configuration if the header is // not set for convenience // return h = CONFIGURATION_LOC; h = h.trim(); if (h.isEmpty()) // // If there is an empty value, we assume // the user does not want it ... // return null; URL url = bundle.getEntry(h); if (url == null) { log.log(LogService.LOG_ERROR, "Cannot find configuration for bundle in " + h); return null; } String s = IO.collect(url); if (s == null) { log.log(LogService.LOG_ERROR, "Cannot find configuration for bundle in " + h); return null; } configure(bundle, s); } catch (IOException e) { log.log(LogService.LOG_ERROR, "Failed to set configuration for " + bundle, e); } // // We do not have to track this, we leave the configuration // in // cm. TODO purge command // return null; } }; tracker.open(); // this will iterate over all bundles synchronously // // Check if we have any extra configuration via system properties // String s = System.getProperty(CONFIGURER_EXTRA); if (s != null) configure(context.getBundle(), s); } catch (Exception e) { coordination.fail(e); } finally { coordination.end(); } } /* * Deactivate */ void deactivate() { tracker.close(); } /* * Main work horse */ static Pattern AT_MACRO_P = Pattern.compile("@\\{(?>.*\\})"); static Pattern MACRO_P = Pattern.compile("(?!\\\\)$\\{(?>.*\\})"); void configure(Bundle bundle, String data) { try { // // First replace @{ ... } with ${...}. This is // optional but allows easy separation from other // macro processors // data = data.replaceAll("(?!\\\\)@\\{", "\\${"); ReplacerAdapter replacer = new ReplacerAdapter(base); replacer.target(this); currentBundle = bundle; data = replacer.process(data); currentBundle = null; if (profile == null) profile = base.containsKey(EN_ROUTE_PROFILE) ? base.get(EN_ROUTE_PROFILE) : "debug"; log.log(LogService.LOG_INFO, "Profile is " + profile); // // Since we need to work with Dictionary, it is convenient to // get the result in a list of Hashtable (which implements // Dictionary). // final List<Hashtable<String,Object>> list = codec.dec().from(data) .get(new TypeReference<List<Hashtable<String,Object>>>() {}); // // Process each dictionary // for (Map<String,Object> d : list) { Hashtable<String,Object> dictionary = new Hashtable<String,Object>(); getDict(d, dictionary); // // We now have a dictionary and should update config admin. // The // dictionary // must contain a pid, and may contain a factory pid. // A factory pid implies that the pid is symbolic since it // will // be assigned // by config admin. // String factory = (String) dictionary.get("service.factoryPid"); String pid = (String) dictionary.get("service.pid"); if (pid == null) { log.log(LogService.LOG_ERROR, "Invalid configuration, no PID specified: " + dictionary); continue; } Configuration configuration; if (factory != null) { // // We have a factory configuration, so the PID is // symbolic // now // dictionary.put(LOGICAL_PID_KEY, pid); dictionary.put(BUNDLE_KEY, bundle.getBundleId()); // // We use the symbolic PID to find an existing record. // if it does not exist, we create a new one. // Configuration instances[] = cm.listConfigurations("(&(" + LOGICAL_PID_KEY + "=" + pid + ")(service.factoryPid=" + factory + "))"); if (instances == null || instances.length == 0) { configuration = cm.createFactoryConfiguration(factory, "?"); } else { configuration = instances[0]; } } else { // // normal target configuration // configuration = cm.getConfiguration(pid, "?"); } configuration.setBundleLocation(null); Dictionary< ? , ? > current = configuration.getProperties(); if (current != null && isEqual(dictionary, current)) continue; configuration.update(dictionary); } } catch (Exception e) { log.log(LogService.LOG_ERROR, "While configuring " + bundle.getBundleId() + ", configuration is " + data, e); } } /* * Build a dictionary from the JSON map we got. We handle the profiles here * by looking at the key, if it is a profile key, and the profile matches, * we add the non-profile key. Otherwise we do some other stuff like logging * and comments. */ private void getDict(Map<String,Object> d, Hashtable<String,Object> dictionary) throws Exception { for (Entry<String,Object> e : d.entrySet()) { String key = e.getKey(); Object value = e.getValue(); Matcher m = PROFILE_PATTERN.matcher(e.getKey()); boolean prfile = false; if (m.matches()) { // // Check if a key is a profile key (starts wit [...] // if so, fix it up. That is, if it matches out // current profile, we remove the prefix and use // it, otherwise we ignore it // String profile = m.group(1); if (!profile.equals(this.profile)) continue; key = m.group(2); prfile = true; } else if (e.getKey().equals(".log")) { // // .log entries are ignored but send to the logger // log.log(LogService.LOG_INFO, converter.convert(String.class, d.get(".log"))); continue; } else if (e.getKey().startsWith(".comment")) // // Keys tha start with .comment are ignored // continue; // // Check if all macros were resolved // if (value != null && value instanceof String) { Matcher matcher = MACRO_P.matcher((String) value); if (matcher.find()) { log.log(LogService.LOG_ERROR, "Configuration has detected macros that are not resolved: " + key + "=" + value); } } // // Profile keys should always override, Otherwise, first one wins // if ( prfile || !dictionary.containsKey(key)) dictionary.put(key, value); } } /* * We do not want to update a configuration unless it really has been * changed */ @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.log(LogService.LOG_INFO, "Updating config because " + a.get("service.pid") + " has a different value for " + e.getKey() + ". Old value " + value + ", new value: " + e.getValue()); return false; } } return true; } /* * This macro gets a resource from the bundle. It will copy this resource * somewhere on the file system. It will return the actual file name. This * is very useful for certificates */ public String _resource(String args[]) throws IOException { if (args.length != 2) { log.log(LogService.LOG_ERROR, "The ${resource} macro only takes 1 argument, the resource path"); return null; } URL url = currentBundle.getEntry(args[1]); if (url == null) { log.log(LogService.LOG_ERROR, "The ${resource;" + args[1] + "} macro cannot find the bundle resource "); return null; } String path = url.getPath(); // // Make sure that the path is safe so someone cannot use this // to access the root or other files outside its data area // if (path.startsWith("/") || path.startsWith("~")) path = path.substring(1); String safe = path.replaceAll("[^\\w\\d._-]|\\.\\.", "_"); File dir = currentBundle.getDataFile(""); File out = IO.getFile(dir, safe); out.getParentFile().mkdirs(); if (!out.getParentFile().isDirectory()) { log.log(LogService.LOG_ERROR, "Cannot create configuration directory " + dir + " in bundle " + currentBundle); } try { IO.copy(url.openStream(), out); } catch (Exception e) { log.log(LogService.LOG_ERROR, "Cannot copy a resource " + out + " from bundle " + currentBundle + " resource " + url); } return out.getAbsolutePath(); } /* * Macro to provide current bundle id */ public String _bundleid(String args[]) { if (args.length != 1) { log.log(LogService.LOG_ERROR, "The ${bundleid} macro takes no parameters"); return null; } return currentBundle.getBundleId() + ""; } /* * Macro to provide current location */ public String _location(String args[]) { if (args.length != 1) { log.log(LogService.LOG_ERROR, "The ${location} macro takes no parameters"); return null; } return currentBundle.getLocation(); } @Reference void setLogService(LogService log) { this.log = log; } @Reference void setCoordinator(Coordinator coordinator) { this.coordinator = coordinator; } @Reference void setCM(ConfigurationAdmin cm) { this.cm = cm; } /* * We need the launcher's arguments to get a profile */ @Reference(cardinality = ReferenceCardinality.OPTIONAL, 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("--profile")) { this.profile = args[i++]; return; } } } void unsetLauncher(Object obj) { } }