/* * RHQ Management Platform * Copyright (C) 2005-2011 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.modules.plugins.wildfly10; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.domain.configuration.Configuration; import org.rhq.core.domain.configuration.ConfigurationUpdateStatus; import org.rhq.core.domain.configuration.Property; import org.rhq.core.domain.configuration.PropertyList; import org.rhq.core.domain.configuration.PropertyMap; import org.rhq.core.domain.configuration.PropertySimple; import org.rhq.core.domain.configuration.definition.ConfigurationDefinition; import org.rhq.core.domain.configuration.definition.PropertyDefinition; import org.rhq.core.domain.configuration.definition.PropertyDefinitionList; import org.rhq.core.domain.configuration.definition.PropertyDefinitionMap; import org.rhq.core.domain.configuration.definition.PropertyDefinitionSimple; import org.rhq.core.domain.configuration.definition.PropertyGroupDefinition; import org.rhq.core.domain.configuration.definition.PropertySimpleType; import org.rhq.core.pluginapi.configuration.ConfigurationFacet; import org.rhq.core.pluginapi.configuration.ConfigurationUpdateReport; import org.rhq.modules.plugins.wildfly10.json.Address; import org.rhq.modules.plugins.wildfly10.json.CompositeOperation; import org.rhq.modules.plugins.wildfly10.json.Operation; import org.rhq.modules.plugins.wildfly10.json.ReadChildrenResources; import org.rhq.modules.plugins.wildfly10.json.ReadResource; import org.rhq.modules.plugins.wildfly10.json.Remove; import org.rhq.modules.plugins.wildfly10.json.Result; import org.rhq.modules.plugins.wildfly10.json.WriteAttribute; public class ConfigurationWriteDelegate implements ConfigurationFacet { /** A map where this error message has been set must not be written to the AS * @see #updateHandlePropertyMapSpecial */ public static final String LOGICAL_REMOVED = "__logicalRemoved"; final Log log = LogFactory.getLog(this.getClass()); protected Address address; protected ASConnection connection; protected ConfigurationDefinition configurationDefinition; private String namePropLocator; private String type; private boolean addNewChildren; private boolean addDeleteModifiedChildren; protected boolean createChildRequested = false; /** * Create a new configuration delegate, that reads the attributes for the resource at address. * @param configDef Configuration definition for the configuration * @param connection asConnection to use * @param address address of the resource. */ public ConfigurationWriteDelegate(ConfigurationDefinition configDef, ASConnection connection, Address address) { this.configurationDefinition = configDef; this.connection = connection; this.address = address; } /** * Trigger loading of a configuration by talking to the remote resource. * @return The initialized configuration * @throws Exception If anything goes wrong. */ public Configuration loadResourceConfiguration() throws Exception { throw new IllegalAccessException("Please use ConfigurationLoadDelegate"); } /** * Write the configuration back to the AS. Care must be taken, not to send properties that * are read-only, as AS will choke on them. * @param report Report containing the new configuration */ public void updateResourceConfiguration(ConfigurationUpdateReport report) { Configuration conf = report.getConfiguration(); CompositeOperation cop = updateGenerateOperationFromProperties(conf, address); Result result = connection.execute(cop); if (!result.isSuccess()) { report.setStatus(ConfigurationUpdateStatus.FAILURE); report.setErrorMessage(result.getFailureDescription()); } else { report.setStatus(ConfigurationUpdateStatus.SUCCESS); // signal "need reload" if (result.isReloadRequired()) { PropertySimple oobMessage = new PropertySimple("__OOB", "The server needs a reload for the latest changes to come effective."); conf.put(oobMessage); } if (result.isRestartRequired()) { PropertySimple oobMessage = new PropertySimple("__OOB", "The server needs a restart for the latest changes to come effective."); conf.put(oobMessage); } } } protected CompositeOperation updateGenerateOperationFromProperties(Configuration conf, Address address) { CompositeOperation cop = new CompositeOperation(); for (PropertyDefinition propDef : configurationDefinition.getNonGroupedProperties()) { updateProperty(conf, cop, propDef, address); } for (PropertyGroupDefinition pgd : configurationDefinition.getGroupDefinitions()) { String groupName = pgd.getName(); namePropLocator = null; if (groupName.startsWith("children:")) { // children, where the key in key=value from the path is known type = groupName.substring("children:".length()); if (type.contains(":")) { namePropLocator = type.substring(type.indexOf(":") + 1); if (namePropLocator.endsWith("+")) { // ending in + -> we need to :add new entries namePropLocator = namePropLocator.substring(0, namePropLocator.length() - 1); addNewChildren = true; } else if (namePropLocator.endsWith("+-")) { // ending in +- -> we need to :add new entries and remove/add to modify namePropLocator = namePropLocator.substring(0, namePropLocator.length() - 2); addNewChildren = true; addDeleteModifiedChildren = true; } else { addNewChildren = false; } type = type.substring(0, type.indexOf(":")); } else { log.error("Group name " + groupName + " contains no property name locator "); return cop; } List<PropertyDefinition> definitions = configurationDefinition.getPropertiesInGroup(groupName); for (PropertyDefinition def : definitions) { updateProperty(conf, cop, def, address); } } else if (groupName.startsWith("child:")) { // one named child resource String subPath = groupName.substring("child:".length()); if (!subPath.contains("=")) throw new IllegalArgumentException("subPath of 'child:' expression has no ="); String condition = null; boolean groupEnabled = true; boolean isEnabledConditionFound = false; if (subPath.contains(":")) { condition = subPath.substring(subPath.indexOf(':') + 1); subPath = subPath.substring(0, subPath.indexOf(':')); // strip off additional trailing options } if (condition != null && condition.startsWith("enabled=")) { isEnabledConditionFound = true; String tmp = condition.substring("enabled=".length()); if (!tmp.contains("=")) throw new IllegalArgumentException("Condition " + condition + " does not have a = between key and value part (" + tmp + ")"); String key = tmp.substring(0, tmp.indexOf('=')); String targetValue = tmp.substring(tmp.indexOf('=') + 1); PropertySimple conditionProperty = conf.getSimple(key); if (conditionProperty != null) { String realValue = conditionProperty.getStringValue(); if (realValue != null && !targetValue.equals(realValue)) groupEnabled = false; } } Address address1 = new Address(address); address1.addSegment(subPath); List<PropertyDefinition> definitions = configurationDefinition.getPropertiesInGroup(groupName); // if this is a single map (not list of maps), then unwind if (definitions.size() == 1 && definitions.get(0) instanceof PropertyDefinitionMap) { PropertyDefinitionMap definitionMap = (PropertyDefinitionMap) definitions.get(0); String mapName = definitionMap.getName(); boolean isAddEnabled = mapName.endsWith("+"); if (groupEnabled) { if (isAddEnabled) { // check if this exists already Operation testOp = new ReadResource(address1); Result res = connection.execute(testOp); if (!res.isSuccess()) { // and try to add it Operation add = new Operation("add", address1); res = connection.execute(add); if (!res.isSuccess()) { // Not much we can do here when the add fails log.error("Adding of node " + address1 + " failed"); } } } definitions = new ArrayList<PropertyDefinition>(definitionMap.getOrderedPropertyDefinitions()); PropertyMap map = conf.getMap(mapName); for (PropertyDefinition def : definitions) { createWriteAttribute(cop, address1, def, map.get(def.getName())); } } else { // group is not enabled, but add is, so lets remove the subpath Remove op = new Remove(address1); Result res = connection.execute(op); // Not much we can do here with the result } } else { for (PropertyDefinition def : definitions) { updateProperty(conf, cop, def, address1); } } }// child: case TODO handle attribute: case else {//handle the base case with no special case handling //get the properties from within the group and update as usual. for (PropertyDefinition propDef : configurationDefinition.getPropertiesInGroup(groupName)) { updateProperty(conf, cop, propDef, address); } } } return cop; } private void updateProperty(Configuration conf, CompositeOperation cop, PropertyDefinition propDef, Address baseAddress) { // Skip over read-only properties, the AS can not use them anyway if (propDef.isReadOnly()) return; // Handle the special case String propDefName = propDef.getName(); if (propDef instanceof PropertyDefinitionList && propDefName.startsWith("*")) { propDef = ((PropertyDefinitionList) propDef).getMemberDefinition(); PropertyList pl = (PropertyList) conf.get(propDefName); // check if we need to see if that property exists - get the current state of affairs from the AS List<String> existingPropnames = new ArrayList<String>(); if (addNewChildren) { Operation op = new ReadChildrenResources(baseAddress, type); Result tmp = connection.execute(op); if (tmp.isSuccess()) { Map<String, Object> tmpResMap = (Map<String, Object>) tmp.getResult(); existingPropnames.addAll(tmpResMap.keySet()); } } // Loop over the list - i.e. the individual rows that come from the server for (Property prop2 : pl.getList()) { updateHandlePropertyMapSpecial(cop, (PropertyMap) prop2, (PropertyDefinitionMap) propDef, baseAddress, existingPropnames); } // now check about removed properties for (String existingName : existingPropnames) { boolean found = false; for (Property prop2 : pl.getList()) { PropertyMap propMap2 = (PropertyMap) prop2; String itemName = propMap2.getSimple(namePropLocator).getStringValue(); if (itemName == null) { throw new IllegalArgumentException("Map contains no entry with name [" + namePropLocator + "]"); } if (itemName.equals(existingName)) { found = true; break; } } // We may still have an entry in the map, that does not // match an existing name, but is marked as immutable if (!found) { for (Property prop2 : pl.getList()) { PropertyMap propMap2 = (PropertyMap) prop2; String itemName = propMap2.getSimple(namePropLocator).getStringValue(); String errorMessage = propMap2.getErrorMessage(); boolean contains = existingPropnames.contains(itemName); if (!contains && LOGICAL_REMOVED.equals(errorMessage)) { found = true; // we pretend this still exists on the server, so nothing to update } } } // In properties on server and not in map or immutable, lets remove it if (!found) { Address tmpAddr = new Address(baseAddress); tmpAddr.add(type, existingName); Operation operation = new Operation("remove", tmpAddr); cop.addStep(operation); } } } else { // Normal cases Property prop = conf.get(propDefName); createWriteAttribute(cop, baseAddress, propDef, prop); } } private void createWriteAttribute(CompositeOperation cop, Address baseAddress, PropertyDefinition propDef, Property prop) { if (prop instanceof PropertySimple && propDef instanceof PropertyDefinitionSimple) { createWriteAttributePropertySimple(cop, (PropertySimple) prop, (PropertyDefinitionSimple) propDef, baseAddress); } else if (prop instanceof PropertyList && propDef instanceof PropertyDefinitionList) { createWriteAttributePropertyList(cop, (PropertyList) prop, (PropertyDefinitionList) propDef, baseAddress); } else if (prop instanceof PropertyMap && propDef instanceof PropertyDefinitionMap) { createWriteAttributePropertyMap(cop, (PropertyMap) prop, (PropertyDefinitionMap) propDef, baseAddress); } else { String s = "Property and definition are not matching:\n"; s += "Property: " + prop + "\n"; s += "PropDef : " + propDef; throw new IllegalArgumentException(s); } } private void updateHandlePropertyMapSpecial(CompositeOperation cop, PropertyMap prop, PropertyDefinitionMap propDef, Address address, List<String> existingPropNames) { if (prop == null) return; // Don't try to send this map to the server if (LOGICAL_REMOVED.equals(prop.getErrorMessage())) return; Map<String, Object> results = prepareSimplePropertyMap(prop, propDef); if (prop.get(namePropLocator) == null) { throw new IllegalArgumentException("There is no element in the map with the name " + namePropLocator); } String key = ((PropertySimple) prop.get(namePropLocator)).getStringValue(); Operation operation; Address addr = new Address(address); addr.add(type, key); if (!addNewChildren || existingPropNames.contains(key)) { // update existing entry if (addDeleteModifiedChildren) { operation = new Remove(addr); cop.addStep(operation); operation = new Operation("add", addr); for (Map.Entry<String, Object> entry : results.entrySet()) { String key1 = entry.getKey(); Object value = getValueWithType(entry, propDef); if (key1.endsWith(":expr")) { key1 = key1.substring(0, key1.indexOf(':')); Map<String, Object> tmp = new HashMap<String, Object>(); tmp.put("EXPRESSION_VALUE", value); operation.addAdditionalProperty(key1, tmp); } else { operation.addAdditionalProperty(key1, value); } } cop.addStep(operation); } else { for (Map.Entry<String, Object> entry : results.entrySet()) { String key1 = entry.getKey(); Object value = getValueWithType(entry, propDef); if (key1.endsWith(":expr")) { key1 = key1.substring(0, key1.indexOf(':')); Map<String, Object> tmp = new HashMap<String, Object>(); tmp.put("EXPRESSION_VALUE", value); operation = new WriteAttribute(addr, key1, tmp); } else { operation = new WriteAttribute(addr, key1, value); } cop.addStep(operation); } } } else { // write new child ( :name+ case ) operation = new Operation("add", addr); for (Map.Entry<String, Object> entry : results.entrySet()) { String key1 = entry.getKey(); Object value = getValueWithType(entry, propDef); if (key1.endsWith(":expr")) { key1 = key1.substring(0, key1.indexOf(':')); Map<String, Object> tmp = new HashMap<String, Object>(); tmp.put("EXPRESSION_VALUE", value); operation.addAdditionalProperty(key1, tmp); } else { operation.addAdditionalProperty(key1, value); } } cop.addStep(operation); } } private void createWriteAttributePropertySimple(CompositeOperation cop, PropertySimple property, PropertyDefinitionSimple propertyDefinition, Address address) { if (property.getName().endsWith(":ignore")) // Caller takes care return; if (propertyDefinition.isReadOnly() && !createChildRequested) return; //If the property value is null and the property is optional, //then send default value or null to the server if (property.getStringValue() == null && !propertyDefinition.isRequired()) { String name = property.getName(); if (name.indexOf(':') != -1) { name = name.substring(0, name.indexOf(":")); } Operation writeAttribute = new WriteAttribute(address, name, null); cop.addStep(writeAttribute); return; } SimpleEntry<String, Object> entry = this.preparePropertySimple(property, propertyDefinition); Operation writeAttribute = new WriteAttribute(address, entry.getKey(), entry.getValue()); cop.addStep(writeAttribute); } private void createWriteAttributePropertyList(CompositeOperation cop, PropertyList property, PropertyDefinitionList propertyDefinition, Address address) { SimpleEntry<String, List<Object>> entry = preparePropertyList(property, propertyDefinition); Operation writeAttribute = new WriteAttribute(address, entry.getKey(), entry.getValue()); cop.addStep(writeAttribute); } private void createWriteAttributePropertyMap(CompositeOperation cop, PropertyMap property, PropertyDefinitionMap propertyDefinition, Address address) { // A map of simples can be me an empty map if all of the simples are optional and unset SimpleEntry<String, Map<String, Object>> entry = this.preparePropertyMap(property, propertyDefinition); Operation writeAttribute = new WriteAttribute(address, entry.getKey(), entry.getValue()); cop.addStep(writeAttribute); } /** * Simple property parsing. * * @param property raw simple property * @param propertyDefinition property definition * @return parsed simple property */ protected SimpleEntry<String, Object> preparePropertySimple(PropertySimple property, PropertyDefinitionSimple propertyDefinition) { SimpleEntry<String, Object> entry = null; String name = stripNumberIdentifier(property.getName()); if (name.endsWith(":expr")) { String realName = name.substring(0, name.indexOf(":")); if (property.getStringValue() != null) { try { if (propertyDefinition.getType().equals(PropertySimpleType.LONG)) { Long num = Long.parseLong(property.getStringValue()); } else { Integer num = Integer.parseInt(property.getStringValue()); } entry = new SimpleEntry<String, Object>(realName, property.getStringValue()); } catch (NumberFormatException nfe) { // Not a number, and expressions are allowed, so send an expression Map<String, String> expr = new HashMap<String, String>(1); expr.put("EXPRESSION_VALUE", property.getStringValue()); entry = new SimpleEntry<String, Object>(realName, expr); } } else { entry = new SimpleEntry<String, Object>(realName, null); } } else { Object o; /* If no value is given in the property and the property is required, we'll take the default value from the definition. This can e.g. happen when you have <c:simple-property name="mode" required="true" type="string" readOnly="false" default="SYNC" defaultValue="SYNC"> <c:property-options> <c:option value="SYNC"/> <c:option value="ASYNC"/> </c:property-options> </c:simple-property> and the user chooses to just keep the default choice in the ui */ if (property.getStringValue() == null && propertyDefinition.isRequired()) { o = getObjectWithType(propertyDefinition, propertyDefinition.getDefaultValue()); } else { o = getObjectWithType(propertyDefinition, property.getStringValue()); } entry = new SimpleEntry<String, Object>(name, o); } return entry; } /** * List property parsing. * * @param property raw list property * @param propertyDefinition property definition * @return parsed list */ protected SimpleEntry<String, List<Object>> preparePropertyList(PropertyList property, PropertyDefinitionList propertyDefinition) { PropertyDefinition memberDef = propertyDefinition.getMemberDefinition(); List<Property> embeddedProps = property.getList(); String propertyName = property.getName(); if (propertyName.endsWith(":nullable")) { propertyName = propertyName.substring(0, propertyName.indexOf(":nullable")); if (embeddedProps.isEmpty()) { return new SimpleEntry<String, List<Object>>(propertyName, null); } } List<Object> values = new ArrayList<Object>(); for (Property inner : embeddedProps) { if (memberDef instanceof PropertyDefinitionSimple) { PropertySimple ps = (PropertySimple) inner; if (ps.getStringValue() != null) values.add(ps.getStringValue()); // TODO handling of optional vs required } if (memberDef instanceof PropertyDefinitionMap) { Map<String, Object> mapResult = null; if (memberDef.getName().endsWith(":collapsed")) { mapResult = prepareCollapsedPropertyMap((PropertyMap) inner, (PropertyDefinitionMap) memberDef); } else { mapResult = prepareSimplePropertyMap((PropertyMap) inner, (PropertyDefinitionMap) memberDef); } values.add(mapResult); } } propertyName = stripNumberIdentifier(propertyName); return new SimpleEntry<String, List<Object>>(propertyName, values); } /** * Map property parsing. * * @param property raw map property * @param propertyDefinition property definition * @return otherwise the parsed map, note that the map can be empty if no map properties were set */ protected SimpleEntry<String, Map<String, Object>> preparePropertyMap(PropertyMap property, PropertyDefinitionMap propertyDefinition) { Map<String, Object> results; String propName = stripNumberIdentifier(property.getName()); if (propName.endsWith(":collapsed")) { propName = propName.substring(0, propName.indexOf(':')); results = prepareCollapsedPropertyMap(property, propertyDefinition); } else { results = prepareSimplePropertyMap(property, propertyDefinition); } return new SimpleEntry<String, Map<String, Object>>(propName, results); } /** * Collapsed map property parsing. * * @param property raw map property * @param propertyDefinition property definition * @return parsed map */ protected Map<String, Object> prepareCollapsedPropertyMap(PropertyMap property, PropertyDefinitionMap propertyDefinition) { String key = null; String value = null; for (Map.Entry<String, PropertyDefinition> entry : propertyDefinition.getMap().entrySet()) { PropertyDefinition def = entry.getValue(); if (!def.getName().contains(":")) throw new IllegalArgumentException("Member names in a :collapsed map must end in :0 and :1"); Property prop = property.get(def.getName()); if (prop == null) { throw new IllegalArgumentException("Property " + def.getName() + " was null - must not happen"); } PropertySimple ps = (PropertySimple) prop; if (def.getName().endsWith(":0")) key = ps.getStringValue(); else if (def.getName().endsWith(":1")) value = ps.getStringValue(); // TODO other types? else throw new IllegalArgumentException("Member names in a :collapsed map must end in :0 and :1"); } if (key != null) { Map<String, Object> resultMap = new HashMap<String, Object>(); resultMap.put(key, value); return resultMap; } else { return null; } } /** * Simple map property parsing. * * @param property raw map property * @param propertyDefinition property definition * @return parsed map, can be empty if no members were set. */ protected Map<String, Object> prepareSimplePropertyMap(PropertyMap property, PropertyDefinitionMap propertyDefinition) { Map<String, PropertyDefinition> memberDefinitions = propertyDefinition.getMap(); Map<String, Object> results = new HashMap<String, Object>(); for (String name : memberDefinitions.keySet()) { PropertyDefinition memberDefinition = memberDefinitions.get(name); if (memberDefinition.isReadOnly()) continue; if (memberDefinition instanceof PropertyDefinitionSimple) { PropertyDefinitionSimple pds = (PropertyDefinitionSimple) memberDefinition; PropertySimple ps = (PropertySimple) property.get(name); if ((ps == null || ps.getStringValue() == null) && !pds.isRequired()) continue; if (ps != null) results.put(name, ps.getStringValue()); } else { log.error(" *** not yet supported *** : " + memberDefinition.getName()); } } return results; } private Object getValueWithType(Map.Entry<String, Object> entry, PropertyDefinitionMap definitions) { PropertyDefinitionSimple pds = (PropertyDefinitionSimple) definitions.get(entry.getKey()); if (!(entry.getValue() instanceof String)) { return entry.getValue(); } String val = (String) entry.getValue(); Object ret = getObjectWithType(pds, val); return ret; } private Object getObjectWithType(PropertyDefinitionSimple pds, String val) { PropertySimpleType type = pds.getType(); Object ret; switch (type) { case STRING: ret = val; break; case INTEGER: ret = Integer.valueOf(val); break; case BOOLEAN: ret = Boolean.valueOf(val); break; case LONG: ret = Long.valueOf(val); break; case FLOAT: ret = Float.valueOf(val); break; case DOUBLE: ret = Double.valueOf(val); break; default: ret = val; } return ret; } /** * Strip :number from the property name. * The post-fixed number was added in the descriptor as unique identifier but it is not * needed (and will result in an error) when writing the property back to AS configuration. * * @param name property name * @return */ private String stripNumberIdentifier(String name) { if (name.contains(":")) { try { Integer.parseInt(name.substring(name.lastIndexOf(':') + 1)); name = name.substring(0, name.lastIndexOf(':')); } catch (Exception e) { //do nothing, this means the property name does not end with :number, so nothing needs to be stripped } } return name; } }