/* * JBoss, Home of Professional Open Source. * Copyright 2012, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.model.test; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NAME; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.VALUE; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import org.jboss.as.controller.AttributeDefinition; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.operations.common.Util; import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelType; import org.jboss.dmr.Property; import org.jboss.dmr.ValueExpression; /** * Sets up how to handle failed transformation for use with * {@link ModelTestUtils#checkFailedTransformedAddOperation(ModelTestKernelServices, org.jboss.as.controller.ModelVersion, ModelNode, FailedOperationTransformationConfig)} * * * @author <a href="kabir.khan@jboss.com">Kabir Khan</a> */ public class FailedOperationTransformationConfig { public static final FailedOperationTransformationConfig NO_FAILURES = new FailedOperationTransformationConfig(); private PathAddressConfigRegistry registry = new PathAddressConfigRegistry(); private boolean transformComposite = true; private BeforeExecuteCompositeCallback callback; /** * Add a handler for failed operation transformers at a resource address * * @param pathAddress the path address * @param config the config * @return this config */ public FailedOperationTransformationConfig addFailedAttribute(PathAddress pathAddress, PathAddressConfig config) { registry.addConfig(pathAddress.iterator(), config); return this; } /** * THe default behaviour is to add the original operations to a composite, once they have been 'fixed'. That composite * is then transformed before executing it in the legacy controller. However some transformers rely on inspecting * the model, and use a {@code config} that changes the model. For these cases, we can call this method, which instead * adds the transformed and fully 'fixed' operations to the resulting composite. * * @return this config */ public FailedOperationTransformationConfig setDontTransformComposite() { this.transformComposite = false; return this; } /** * Sets a callback that gets executed before transforming and executing the resulting composite. * * @param callback the callback * @return this config */ public FailedOperationTransformationConfig setCallback(BeforeExecuteCompositeCallback callback) { this.callback = callback; return this; } boolean isTransformComposite() { return transformComposite; } void invokeCallback() { if (callback != null) { callback.callback(); } } boolean expectFailed(ModelNode operation) { PathAddressConfig cfg = registry.getConfig(operation); if (cfg == null) { return false; } if (cfg.expectFailed(operation)) { return true; } return false; } boolean expectDiscarded(ModelNode operation) { PathAddressConfig cfg = registry.getConfig(operation); if (cfg == null) { return false; } if (cfg.expectDiscarded(operation)) { return true; } return false; } boolean canCorrectMore(ModelNode operation) { PathAddressConfig cfg = registry.getConfig(operation); if (cfg == null) { return false; } if (cfg.canCorrectMore(operation)) { return true; } return false; } ModelNode correctOperation(ModelNode operation) { PathAddressConfig cfg = registry.getConfig(operation); if (cfg == null) { throw new IllegalStateException("No path address config found for " + PathAddress.pathAddress(operation.get(OP_ADDR))); } operation = cfg.correctOperation(operation); return operation; } List<ModelNode> createWriteAttributeOperations(ModelNode operation) { PathAddressConfig cfg = registry.getConfig(operation); if (cfg == null || !operation.get(OP).asString().equals(ADD)) { return Collections.emptyList(); } return cfg.createWriteAttributeOperations(operation); } boolean expectFailedWriteAttributeOperation(ModelNode operation) { PathAddressConfig cfg = registry.getConfig(operation); if (cfg == null) { return false; } return cfg.expectFailedWriteAttributeOperation(operation); } ModelNode correctWriteAttributeOperation(ModelNode operation) { PathAddressConfig cfg = registry.getConfig(operation); if (cfg == null) { return operation; } return cfg.correctWriteAttributeOperation(operation); } public void operationDone(ModelNode operation) { PathAddressConfig cfg = registry.getConfig(operation); if (cfg == null) { return; } cfg.operationDone(operation); } private static class PathAddressConfigRegistry { private final Map<PathElement,PathAddressConfigRegistry> children = new HashMap<PathElement, FailedOperationTransformationConfig.PathAddressConfigRegistry>(); private final Map<String, PathAddressConfigRegistry> wildcardChildren = new HashMap<String, FailedOperationTransformationConfig.PathAddressConfigRegistry>(); private PathAddressConfig config; void addConfig(Iterator<PathElement> address, PathAddressConfig config) { if (address.hasNext()) { PathAddressConfigRegistry child = getOrCreateConfig(address.next()); child.addConfig(address, config); } else { this.config = config; } } PathAddressConfigRegistry getOrCreateConfig(PathElement element) { if (element.isWildcard()) { PathAddressConfigRegistry reg = wildcardChildren.get(element.getKey()); if (reg == null) { reg = new PathAddressConfigRegistry(); wildcardChildren.put(element.getKey(), reg); } return reg; } else { PathAddressConfigRegistry reg = children.get(element); if (reg == null) { reg = new PathAddressConfigRegistry(); children.put(element, reg); } return reg; } } PathAddressConfig getConfig(ModelNode operation) { return getConfig(PathAddress.pathAddress(operation.get(OP_ADDR)).iterator()); } PathAddressConfig getConfig(Iterator<PathElement> address) { if (address.hasNext()) { PathElement element = address.next(); PathAddressConfigRegistry child = children.get(element); if (child == null) { child = wildcardChildren.get(element.getKey()); } if (child != null) { return child.getConfig(address); } return null; } else { return config; } } } /** * Configuration of how to deal with rejected operations transformations for a PathAddress. * See {@link ModelTestUtils#checkFailedTransformedBootOperations(ModelTestKernelServices, org.jboss.as.controller.ModelVersion, List, FailedOperationTransformationConfig)} * for the mechanics of how it is used */ public interface PathAddressConfig { /** * Whether it is expected that the following operation should fail * * @param operation the operation to check * @return {@code true} if expected to fail */ boolean expectFailed(ModelNode operation); /** * Whether something can be corrected in the operation to make it pass. * It is preferable to correct one attribute at a time. * * @param operation the operation to check * @return {@code true} if expected to fail, {@code false} otherwise */ boolean canCorrectMore(ModelNode operation); /** * Correct the operation, only called if {@link #canCorrectMore(ModelNode)} returned {@code true} * It is preferable to correct one attribute at a time. * * @param operation the operation to correct * @return the corrected operation */ ModelNode correctOperation(ModelNode operation); /** * Creates write attribute operations for the add operations */ List<ModelNode> createWriteAttributeOperations(ModelNode operation); /** * Whether it is expected that the following write attribute operation should fail * * @param operation the 'add' operation to correct * @return {@code true} if expected to fail */ boolean expectFailedWriteAttributeOperation(ModelNode operation); /** * Correct the operation. This only deals with one attribute, and * the framework will only call this once if it failed and {@link #correctWriteAttributeOperation(ModelNode)} * returned {@code true}, so make sure to do everything to correct the {@code value} attribute. * * @param operation the 'write-attribute' operation to correct * @return the corrected operation */ ModelNode correctWriteAttributeOperation(ModelNode operation); /** * Whether the operation is expected to be discarded * * @param operation the operation to check * @return {@code true} if expected to fail */ boolean expectDiscarded(ModelNode operation); /** * Called by the framework when the operation has been all fixed up. * * @param operation the operation */ default void operationDone(ModelNode operation) { } } public abstract static class AttributesPathAddressConfig<T extends AttributesPathAddressConfig<T>> implements PathAddressConfig { protected final Set<String> attributes; protected final Map<String, AttributesPathAddressConfig<?>> complexAttributes = new HashMap<String, AttributesPathAddressConfig<?>>(); protected final Set<String> noWriteFailureAttributes = new HashSet<String>(); protected final Set<String> readOnlyAttributes = new HashSet<String>(); protected AttributesPathAddressConfig(String...attributes) { this.attributes = new HashSet<String>(Arrays.asList(attributes)); } public AttributesPathAddressConfig<T> configureComplexAttribute(String attribute, T childConfig) { if (!attributes.contains(attribute)) { throw new IllegalStateException("Attempt to configure a complex attribute that was not set up as one of the original attributes"); } complexAttributes.put(attribute, childConfig); return this; } protected static String[] convert(AttributeDefinition...defs) { String[] attrs = new String[defs.length]; for (int i = 0 ; i < defs.length ; i++) { attrs[i] = defs[i].getName(); } return attrs; } public List<ModelNode> createWriteAttributeOperations(ModelNode operation) { List<ModelNode> list = new ArrayList<ModelNode>(); for (String attr : attributes) { if (operation.hasDefined(attr)) { //TODO Should we also allow undefined here? if (!readOnlyAttributes.contains(attr) && isAttributeWritable(attr)) { list.add(Util.getWriteAttributeOperation(PathAddress.pathAddress(operation.get(OP_ADDR)), attr, operation.get(attr))); } } } return list; } protected abstract boolean isAttributeWritable(String attributeName); public AttributesPathAddressConfig<T> setNotExpectedWriteFailure(String...attributes) { for (String attr : attributes) { noWriteFailureAttributes.add(attr); } return this; } public AttributesPathAddressConfig<T> setNotExpectedWriteFailure(AttributeDefinition...attributes) { for (AttributeDefinition attr : attributes) { noWriteFailureAttributes.add(attr.getName()); } return this; } public AttributesPathAddressConfig<T> setReadOnly(String...attributes) { for (String attr : attributes) { readOnlyAttributes.add(attr); } return this; } public AttributesPathAddressConfig<T> setReadOnly(AttributeDefinition...attributes) { for (AttributeDefinition attr : attributes) { readOnlyAttributes.add(attr.getName()); } return this; } @Override public boolean expectFailed(ModelNode operation) { ModelNode op = operation.clone(); ModelNode checkOp = op.clone(); checkOp.protect(); for (String attr : attributes) { if (checkValue(checkOp, attr, op.get(attr), false)) { return true; } } return false; } @Override public boolean canCorrectMore(ModelNode operation) { ModelNode op = operation.clone(); ModelNode checkOp = op.clone(); checkOp.protect(); for (String attr : attributes) { if (checkValue(checkOp, attr, op.get(attr), false)) { return true; } } return false; } @Override public boolean expectFailedWriteAttributeOperation(ModelNode operation) { ModelNode checkOp = operation.clone(); checkOp.protect(); String name = operation.get(NAME).asString(); if (attributes.contains(name)) { return !noWriteFailureAttributes.contains(name) && checkValue(checkOp, name, operation.clone().get(VALUE), true); } return false; } @Override public ModelNode correctOperation(ModelNode operation) { ModelNode op = operation.clone(); for (String attr : attributes) { ModelNode value = op.get(attr); ModelNode checkOp = op.clone(); checkOp.protect(); if (checkValue(checkOp, attr, value, false)) { AttributesPathAddressConfig<?> complexChildConfig = complexAttributes.get(attr); if (complexChildConfig == null) { ModelNode resolved = correctValue(op.get(attr), false); op.get(attr).set(resolved); } else { op.get(attr).set(complexChildConfig.correctOperation(operation.get(attr))); } return op; } } return operation; } protected boolean correctUndefinedValue() { return false; } @Override public ModelNode correctWriteAttributeOperation(ModelNode operation) { ModelNode op = operation.clone(); ModelNode checkOp = op.clone(); checkOp.protect(); String name = operation.get(NAME).asString(); if (attributes.contains(name) && checkValue(checkOp, name, op.get(VALUE), true)) { op.get(VALUE).set(correctValue(op.get(VALUE), true)); return op; } return operation; } @Override public boolean expectDiscarded(ModelNode operation) { return false; } @SuppressWarnings("unused") protected boolean checkValue(ModelNode operation, String attrName, ModelNode attribute, boolean isGeneratedWriteAttribute) { return checkValue(attrName, attribute, isGeneratedWriteAttribute); } protected abstract boolean checkValue(String attrName, ModelNode attribute, boolean isGeneratedWriteAttribute); protected abstract ModelNode correctValue(ModelNode toResolve, boolean isGeneratedWriteAttribute); } /** * A standard configuration for the reject expression values transformer * */ public static class RejectExpressionsConfig extends AttributesPathAddressConfig<RejectExpressionsConfig> { private final Pattern EXPRESSION_PATTERN = Pattern.compile(".*\\$\\{.*\\}.*"); public RejectExpressionsConfig(String...attributes) { super(attributes); } public RejectExpressionsConfig(AttributeDefinition...attributes) { super(convert(attributes)); } protected ModelNode correctValue(ModelNode toResolve, boolean isGeneratedWriteAttribute) { if (toResolve.getType() == ModelType.STRING) { toResolve = new ModelNode().set(new ValueExpression(toResolve.asString())); } return toResolve.resolve(); } @Override protected boolean isAttributeWritable(String attributeName) { return true; } @Override protected boolean checkValue(String attrName, ModelNode attribute, boolean isGeneratedWriteAttribute) { if (!attribute.isDefined()) { return false; } AttributesPathAddressConfig<?> complexChildConfig = complexAttributes.get(attrName); switch (attribute.getType()) { case EXPRESSION: return true; case STRING: return EXPRESSION_PATTERN.matcher(attribute.asString()).matches(); case LIST: for (ModelNode entry : attribute.asList()) { if (complexChildConfig == null) { if (checkValue(attrName, entry, isGeneratedWriteAttribute)) { return true; } } else { if (childHasExpressions(complexChildConfig, attribute.get(attrName), isGeneratedWriteAttribute)) { return true; } } } break; case OBJECT: for (Property prop : attribute.asPropertyList()) { if (complexChildConfig == null) { if (checkValue(attrName, prop.getValue(), isGeneratedWriteAttribute)) { return true; } } else { if (childHasExpressions(complexChildConfig, attribute, isGeneratedWriteAttribute)) { return true; } } } break; case PROPERTY: if (complexChildConfig == null) { if (checkValue(attrName, attribute.asProperty().getValue(), isGeneratedWriteAttribute)) { return true; } } else { if (childHasExpressions(complexChildConfig, attribute.asProperty().getValue(), isGeneratedWriteAttribute)) { return true; } } } return false; } private boolean childHasExpressions(AttributesPathAddressConfig<?> complexChildConfig, ModelNode attribute, boolean isGeneratedWriteAttribute) { for (String child : complexChildConfig.attributes) { if (complexChildConfig.checkValue(child, attribute.get(child), isGeneratedWriteAttribute)) { return true; } } return false; } } /** * A standard configuration for the {@link org.jboss.as.controller.transform.DiscardUndefinedAttributesTransformer} * for use with attributes that are new in a version. */ public static class NewAttributesConfig extends AttributesPathAddressConfig<NewAttributesConfig> { public NewAttributesConfig(String...attributes) { super(attributes); } public NewAttributesConfig(AttributeDefinition...attributes) { super(convert(attributes)); } @Override protected boolean checkValue(String attrName, ModelNode attribute, boolean isGeneratedWriteAttribute) { return attribute.isDefined(); } @Override protected ModelNode correctValue(ModelNode attribute, boolean isGeneratedWriteAttribute) { return new ModelNode(); } @Override protected boolean isAttributeWritable(String attributeName) { return true; } } /** * A standard configuration that allows several checkers to be used for an attribute. * For proper test coverage, the configs should be added in the same order as the rejecting transformers */ public static class ChainedConfig extends AttributesPathAddressConfig<ChainedConfig> { private final List<AttributesPathAddressConfig<?>> list = new ArrayList<FailedOperationTransformationConfig.AttributesPathAddressConfig<?>>(); /** * Constructor * * @param configs the configurations to use. For proper test coverage, these should be added in the same order as the rejecting transformers * @param attributes the attributes to apply these transformers to */ public ChainedConfig(List<AttributesPathAddressConfig<?>> configs, String...attributes) { super(attributes); this.list.addAll(configs); } /** * Constructor * * @param configs the configurations to use. For proper test coverage, these should be added in the same order as the rejecting transformers * @param attributes the attributes to apply these transformers to */ public ChainedConfig(List<AttributesPathAddressConfig<?>> configs, AttributeDefinition...attributes) { super(convert(attributes)); this.list.addAll(configs); } @Override public boolean expectFailed(ModelNode operation) { for (AttributesPathAddressConfig<?> link : list) { if (link.expectFailed(operation)) { return true; } } return false; } @Override public ModelNode correctOperation(ModelNode operation) { for (AttributesPathAddressConfig<?> link : list) { ModelNode op = link.correctOperation(operation); if (!op.equals(operation)) { return op; } } return operation; } @Override public ModelNode correctWriteAttributeOperation(ModelNode operation) { ModelNode op = operation.clone(); for (AttributesPathAddressConfig<?> link : list) { op = link.correctWriteAttributeOperation(op); } return op; } @Override protected boolean isAttributeWritable(String attributeName) { return true; } @Override public boolean canCorrectMore(ModelNode operation) { for (AttributesPathAddressConfig<?> link : list) { if (link.canCorrectMore(operation)) { return true; } } return false; } @Override public boolean expectFailedWriteAttributeOperation(ModelNode operation) { if (!noWriteFailureAttributes.contains(operation.get(NAME).asString())) { for (AttributesPathAddressConfig<?> link : list) { if (link.expectFailedWriteAttributeOperation(operation)) { return true; } } } return false; } @Override protected boolean checkValue(String attrName, ModelNode attribute, boolean isGeneratedWriteAttribute) { //Since all the PathAddress methods have been overridden this should never be called throw new IllegalStateException("Not all methods were overridden"); } @Override protected ModelNode correctValue(ModelNode toResolve, boolean isGeneratedWriteAttribute) { //Since all the PathAddress methods have been overridden this should never be called throw new IllegalStateException("Not all methods were overridden"); } public interface Builder { Builder addConfig(AttributesPathAddressConfig<?> cfg); ChainedConfig build(); } public static Builder createBuilder(final String...attributes) { return new Builder() { ArrayList<AttributesPathAddressConfig<?>> list = new ArrayList<FailedOperationTransformationConfig.AttributesPathAddressConfig<?>>(); @Override public ChainedConfig build() { return new ChainedConfig(list, attributes); } @Override public Builder addConfig(AttributesPathAddressConfig<?> cfg) { list.add(cfg); return this; } }; } public static Builder createBuilder(AttributeDefinition...attributes) { return createBuilder(convert(attributes)); } } public static final PathAddressConfig DISCARDED_RESOURCE = new PathAddressConfig() { @Override public boolean expectFailedWriteAttributeOperation(ModelNode operation) { throw new IllegalStateException("Should not get called"); } @Override public boolean expectFailed(ModelNode operation) { return false; } @Override public boolean expectDiscarded(ModelNode operation) { return true; } @Override public List<ModelNode> createWriteAttributeOperations(ModelNode operation) { return Collections.emptyList(); } @Override public ModelNode correctWriteAttributeOperation(ModelNode operation) { throw new IllegalStateException("Should not get called"); } @Override public ModelNode correctOperation(ModelNode operation) { throw new IllegalStateException("Should not get called"); } @Override public boolean canCorrectMore(ModelNode operation) { throw new IllegalStateException("Should not get called"); } }; public static final PathAddressConfig REJECTED_RESOURCE = new PathAddressConfig() { @Override public boolean expectFailedWriteAttributeOperation(ModelNode operation) { throw new IllegalStateException("Should not get called"); } @Override public boolean expectFailed(ModelNode operation) { return true; } @Override public boolean expectDiscarded(ModelNode operation) { //The reject simply forwards on the original operation to make it fail return false; } @Override public List<ModelNode> createWriteAttributeOperations(ModelNode operation) { return Collections.emptyList(); } @Override public ModelNode correctWriteAttributeOperation(ModelNode operation) { throw new IllegalStateException("Should not get called"); } @Override public ModelNode correctOperation(ModelNode operation) { throw new IllegalStateException("Should not get called"); } @Override public boolean canCorrectMore(ModelNode operation) { return false; } }; /** * A callback that can be used to e.g. make adjustments to the model before transforming and executing the composite * resulting from the transformation of the boot operations */ public interface BeforeExecuteCompositeCallback { void callback(); } }