package org.commons.jconfig.configloader; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.TreeSet; import javax.management.Attribute; import javax.management.AttributeNotFoundException; import javax.management.InstanceNotFoundException; import javax.management.IntrospectionException; import javax.management.InvalidAttributeValueException; import javax.management.MBeanAttributeInfo; import javax.management.MBeanException; import javax.management.MBeanNotificationInfo; import javax.management.MBeanServerConnection; import javax.management.MalformedObjectNameException; import javax.management.NotificationBroadcasterSupport; import javax.management.ObjectName; import javax.management.ReflectionException; import org.apache.log4j.Logger; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.node.ArrayNode; import org.codehaus.jackson.node.ObjectNode; import org.commons.jconfig.config.ConfigException; import org.commons.jconfig.config.ConfigLoaderAdapterID; import org.commons.jconfig.internal.jmx.ConfigManagerJmx; import org.commons.jconfig.internal.jmx.ConfigManagerJvm; import org.commons.jconfig.internal.jmx.LoadAppConfigsNotification; import org.commons.jconfig.internal.jmx.LoadModuleConfigsNotification; import org.commons.jconfig.internal.jmx.VirtualMachineException; import org.commons.jconfig.loader.adapters.Adapter; import org.commons.jconfig.loader.adapters.AutoConf; import org.commons.jconfig.loader.adapters.AutoConfAdapter; import org.commons.jconfig.loader.adapters.LsgAdapter; /** * Implements @ConfigLoaderMXBean and Broadcasts two Notifications<br><br> * * 1. @LoadAppConfigsNotification<br> * 2. @LoadModuleConfigsNotification<br> * * <b>Note</b> Modular ability to load from multiple @ConfigSource.<Br> * currently supports @AutoConf */ public class ConfigLoaderJmx extends NotificationBroadcasterSupport implements ConfigLoaderJmxMXBean { private final Logger logger = Logger.getLogger(ConfigLoaderJmx.class); private ConfigLoaderConfig config; private static final String SETS_TYPE = "_Sets_Type_"; private static final String SETS = "_Sets_"; private static final String SETS_KEY_NODE = "key"; private static final String SETS_KEYLIST_NODE = "keyList"; /** * Empty constructor for registering MBean, since without registering Loader * MBean we cannot get ConfigLoaderConfig instance. */ protected ConfigLoaderJmx() { } /** * @param config * ConfigLoaderConfig instance. */ public ConfigLoaderJmx(ConfigLoaderConfig config) { init(config); } /** * Load up list of known Adapters currently * * @param config * @AutoConfConfigModuleAdapter * @LSGConfigModuleAdapter */ protected void init(final ConfigLoaderConfig config) { this.config = config; AutoConf autoconf = new AutoConf(config); AutoConfAdapter stdrdAdapter = new AutoConfAdapter(autoconf); LsgAdapter lsgAdapter = new LsgAdapter(autoconf); adapterMap.put(stdrdAdapter.getUri(), stdrdAdapter); adapterMap.put(lsgAdapter.getUri(), lsgAdapter); } /** * Notifications broadcast by ConfigLoader */ @Override public MBeanNotificationInfo[] getNotificationInfo() { MBeanNotificationInfo loadAppInfo = new MBeanNotificationInfo( new String[] { LoadAppConfigsNotification.APP_CONFIGS_TYPE}, LoadAppConfigsNotification.class.getName(), LoadAppConfigsNotification.APP_CONFIGS_DESC); MBeanNotificationInfo LoadModuleInfo = new MBeanNotificationInfo( new String[] { LoadModuleConfigsNotification.MODULE_CONFIGS_TYPE}, LoadModuleConfigsNotification.class.getName(), LoadModuleConfigsNotification.MODULE_CONFIGS_DESC); return new MBeanNotificationInfo[] {loadAppInfo, LoadModuleInfo}; } @Override public void subscribeConfigs(final String appName) throws ConfigException { } /** * for the given appName, set the MBeans with configurations<br> * <br> * <b>Note</b> : Config MBean without a corresponding module in the * configuration source is not an error case. Only a warning is logged. it * is the caller's responsibility to verify all modules are populated.<br> * * @param managerObjectName * @param force * if false, only set if the configurations changed from previous * load. else, load regardless. * @throws ConfigException * if appName not found in the configurations source. */ public void loadAppConfigs(final ObjectName managerObjectName, final boolean force) throws VirtualMachineException, ConfigException { String applicationName = managerObjectName.getKeyProperty(ConfigManagerJvm.APPNAME_KEY); // Configuration Manager MBean of the application to load configs for ConfigManagerJvm vm = new ConfigManagerJvm(managerObjectName); //the result of the load operation boolean result = false; boolean sendNotification = true; String notificationMsg = "config loading for " + applicationName + " application is complete"; try { vm.attach(); MBeanServerConnection mbsc = vm.getJMXConnector().getMBeanServerConnection(); // Query MBean in the Config MBeans domain // Set<ObjectName> configNames = new TreeSet<ObjectName>(mbsc.queryNames(new ObjectName( ConfigManagerJmx.CONFIG_MBEANS_SEARCH_PATTERN + applicationName + ",*"), null)); if ( configNames.size() == 0 ) { logger.error("No configuration MBeans registered in " + applicationName); return; } JsonNode appNode = getApplicationConfig(mbsc, applicationName, configNames); logger.debug("loading " + applicationName + " with configs: " + appNode.toString()); // For each registered configuration MBean set the MBean properties for (ObjectName bname : configNames) { try { loadModuleConfigs(mbsc, applicationName, appNode, bname, force); } catch (ConfigException e) { //Swallow the exception. No need to fail the entire app just cause one module is bad. logger.error("no configuration found for the " + moduleName(bname) + " module for the " + applicationName + " applicaton"); } } result = true; } catch (VirtualMachineException e) { throw e; } catch (Exception e) { throw new ConfigException(e); } finally { if (sendNotification) { // Send the notification for this appName LoadAppConfigsNotification n = new LoadAppConfigsNotification(this, sequenceNumber++, System.currentTimeMillis(), notificationMsg, applicationName, result); sendNotification(n); logger.info("Notified application " + applicationName + " about new configs."); } try { vm.close(); } catch (VirtualMachineException e) { logger.error(e.getMessage()); } } } /** * Generate hash value for application config and return 0 if config not * found or any error returned. * * @param vm * {@link ConfigManagerJvm} Assume vm is already attached * @param appName * Application name * @return hash value for applicationConfig value */ public Integer getAppConfigHash(ConfigManagerJvm vm, String appName) { try { MBeanServerConnection mbsc = vm.getJMXConnector().getMBeanServerConnection(); Set<ObjectName> configNames = new TreeSet<ObjectName>(); configNames = new TreeSet<ObjectName>(mbsc.queryNames(new ObjectName( ConfigManagerJmx.CONFIG_MBEANS_SEARCH_PATTERN + appName + ",*"), null)); if (configNames.size() == 0) { return 0; } JsonNode appNode = getApplicationConfig(mbsc, appName, configNames); if (appNode != null) { ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(appNode).hashCode(); } else { return 0; } } catch (ConfigException e) { logger.error("Error getting app config hash ", e); } catch (MalformedObjectNameException e) { logger.error("Error getting app config hash ", e); } catch (IOException e) { logger.error("Error getting app config hash ", e); } return 0; } /** * for the Config MBean in the given appName, set the MBean with * configurations<br> * <br> * <b>Note</b> : Config MBean attributes without a corresponding value in * the configuration source is<br> * not an error case. Only a warning is logged. it is the caller's * responsibility to verify all attributes<br> * are populated.<br> * * @param mbsc * @param appName * @param appNode * @param bname * @param force * if false, only set if the configurations changed from previous * load. else, load regardless. * @throws ConfigException * @throws ConfigException * if configuration module is missing in the JsonNode * @throws IOException * @throws ReflectionException * @throws MBeanException * @throws InvalidAttributeValueException * @throws AttributeNotFoundException * @throws InstanceNotFoundException * @throws IntrospectionException */ private void loadModuleConfigs(final MBeanServerConnection mbsc, final String appName, final JsonNode appNode, final ObjectName bname, final boolean force) throws ConfigException, InstanceNotFoundException, AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException, IOException, IntrospectionException { boolean result = false; boolean sendNotification = true; String module = moduleName(bname); String notificationMsg = "config loading for module " + module + " of " + appName + " application is complete"; try { if ( ! hasModule(appNode, module) ) { notificationMsg = "no configuration found for the " + module + " module for the " + appName + " applicaton"; throw new ConfigException(notificationMsg); } JsonNode moduleNode = getModule(appNode, module); // If we are reSynching, check if the config source configuration changed from our last load Integer checkSum = moduleNode.hashCode(); if ( ! force && checkSum.equals(moduleConfCheckSumMap.get(appName + "." + module)) ) { logger.debug("configuration for the " + module + " module for the " + appName + " applicaton still in synch"); sendNotification = false; return; } logger.info("configuration for the " + module + " module for the " + appName + " applicaton: " + moduleNode.toString()); // update our chucksum for this module moduleConfCheckSumMap.put(appName + "." + module, checkSum); // Set the configuration MBeans attributes in the jmx get the attributes for this MBean MBeanAttributeInfo[] attribs = mbsc.getMBeanInfo(bname).getAttributes(); // Populate the MBean attributes using the config node for (MBeanAttributeInfo attrib : attribs) { if (!attrib.isWritable()) { logger.info(module + " " + attrib.getName() + " MBean attribute is read only"); continue; } if (!hasAttribute(moduleNode, attrib.getName())) { logger.info(module + " " + attrib.getName() + " MBean attribute is missing configuration entry"); continue; } try { mbsc.setAttribute(bname, createConfigAttribute(attrib, moduleNode)); } catch (ConfigException e) { // Swallow the exception. No need to fail the entire MBean // just cause one attribute is bad. logger.info(module + " " + attrib.getName() + " MBean attribute load error"); } } result = true; } finally { if ( sendNotification ) { // // Send the notification for this module LoadModuleConfigsNotification n = new LoadModuleConfigsNotification( this, sequenceNumber++, System.currentTimeMillis(), notificationMsg, appName, module, result); sendNotification(n); logger.info("Notified module " + module + " of application " + appName + " about new configs."); } } } /** * Get the application config Json node for the given given app. null, if * does not exist.<br> * <br> * * builds a Json structure of all the module configs for the given<br> * application. * * @param appName * @param configNames * - list of MBean names to set * @param config * @return * @throws ConfigException * @throws ConfigException */ private JsonNode getApplicationConfig(final MBeanServerConnection mbsc, final String appName, final Set<ObjectName> configNames) throws ConfigException { String moduleName = null; ObjectMapper mapper = new ObjectMapper(); ObjectNode appNode = mapper.createObjectNode(); for (ObjectName bname : configNames) { moduleName = moduleName(bname); Object strAdapter = null; // Check if this config class needs to use a config module adapter try { strAdapter = mbsc.getAttribute(bname, "ConfigLoaderAdapter"); } catch (AttributeNotFoundException e) { logger.error("LoaderAdapter attribute missing for " + moduleName + " Config MBean", e); } catch (InstanceNotFoundException e) { logger.error("LoaderAdapter attribute missing for " + moduleName + " Config MBean", e); } catch (MBeanException e) { logger.error("LoaderAdapter attribute missing for " + moduleName + " Config MBean", e); } catch (ReflectionException e) { logger.error("LoaderAdapter attribute missing for " + moduleName + " Config MBean", e); } catch (IOException e) { logger.error("LoaderAdapter attribute missing for " + moduleName + " Config MBean", e); } // No adapter specified, log it // and assume we are using standard autoConf config module if ( strAdapter == null ) { strAdapter = ConfigLoaderAdapterID.JSON_AUTOCONF.getUri(); } if (strAdapter.equals(ConfigLoaderAdapterID.LSG_AUTOCONF.getUri())) { System.out.println(""); } // Check to see if we have an adapter for this config module Adapter adapter = adapterMap.get(strAdapter); if (adapter != null) { JsonNode moduleNode = adapter.getModuleNode(appName, moduleName); appNode.put(moduleName, moduleNode); } else { throw new ConfigException("Failed to create adapter."); } } return appNode; } /** * Existence of module config Json node for the given module in for the given app. * * @param appNode * @param module * @return */ private static boolean hasModule(final JsonNode appNode, final String module) { return appNode.has(module); } /** * Get the module config Json node for the given module in for the given app. * null, if does not exist. * * @param appNode * @param module * @return */ private static JsonNode getModule(final JsonNode appNode, final String module) { return appNode.get(module); } /** * Existence of attribute node node for the given attribute in the given module. * includes checks for attributes inside "Sets" * * @param moduleNode * @param attribName * @return */ private static boolean hasAttribute(final JsonNode moduleNode, final String attribName) { /* check in default values */ if (moduleNode.has(attribName)) { return true; } if (moduleNode.has(SETS)) { JsonNode sets = moduleNode.get(SETS); if (sets.isArray()) { for (JsonNode node : sets) { if (!node.path(SETS_KEYLIST_NODE).path(attribName).isMissingNode()) { return true; } } } } return false; } /** * Given and attributeInfo and a configuration node, create and return a set * attribute object * * @param attrib * MBean attribute * @param node * configuration node * * @return a populated attribute * @throws ConfigException * if configuration entry is missing in the JsonNode<br> * Or type mismatch between entry in JsonNode and attribute * type. */ private static Attribute createConfigAttribute(final MBeanAttributeInfo attrib, final JsonNode node) throws ConfigException { String attribName = attrib.getName(); ObjectMapper mapper = new ObjectMapper(); ObjectNode rootNode = mapper.createObjectNode(); ArrayNode newSets = mapper.createArrayNode(); if (null != node.get(attribName)) { rootNode.put(attribName, node.get(attribName)); } if (null != node.get(SETS_TYPE)) { rootNode.put(SETS_TYPE, node.get(SETS_TYPE)); JsonNode origSets = node.get(SETS); if ((origSets != null) && origSets.isArray()) { Iterator<JsonNode> itr = origSets.iterator(); while (itr.hasNext()) { JsonNode setElement = itr.next(); JsonNode key = setElement.get(SETS_KEY_NODE); JsonNode keyList = setElement.get(SETS_KEYLIST_NODE); // bad element; skip it if ((key == null) || (keyList == null)) { continue; } Iterator<String> keyListItr = keyList.getFieldNames(); while (keyListItr.hasNext()) { String keyListAttribName = keyListItr.next(); // Is this the attribute name we are interested in if (keyListAttribName.equals(attribName)) { // Build a new node with just the keyList attribute // we // are interested in ObjectNode newNode = mapper.createObjectNode(); ObjectNode attributeNode = mapper.createObjectNode(); attributeNode.put(attribName, keyList.path(attribName)); newNode.put(SETS_KEY_NODE, key); newNode.put(SETS_KEYLIST_NODE, attributeNode); newSets.add(newNode); } } } } rootNode.put(SETS, newSets); } return new Attribute(attrib.getName(), rootNode.toString()); } /** * given MBean ObjectName, return simple string name * * @param bname * @return */ private static String moduleName(final ObjectName bname) { return bname.getKeyProperty("type"); } /** * Notification sequence counter */ private long sequenceNumber = 1; /** * Map of "Application.module" JsonNode references. This cache used to test for * configuration changes on reSycn operations */ private final Map<String, Integer> moduleConfCheckSumMap = new HashMap<String, Integer>(); /** * Map of known config module adapters */ private final Map<String, Adapter> adapterMap = new HashMap<String, Adapter>(); public ConfigLoaderConfig getConfig() { return config; } }