/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.brooklyn.core.mgmt.rebind.transformer; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import java.io.File; import javax.annotation.Nullable; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode; import org.apache.brooklyn.api.mgmt.rebind.PersistenceExceptionHandler; import org.apache.brooklyn.api.mgmt.rebind.RebindExceptionHandler; import org.apache.brooklyn.api.mgmt.rebind.RebindManager.RebindFailureMode; import org.apache.brooklyn.api.mgmt.rebind.mementos.BrooklynMementoRawData; import org.apache.brooklyn.api.objs.BrooklynObjectType; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.BasicConfigKey; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.mgmt.persist.BrooklynMementoPersisterToObjectStore; import org.apache.brooklyn.core.mgmt.persist.FileBasedObjectStore; import org.apache.brooklyn.core.mgmt.persist.PersistMode; import org.apache.brooklyn.core.mgmt.rebind.PersistenceExceptionHandlerImpl; import org.apache.brooklyn.core.mgmt.rebind.RebindOptions; import org.apache.brooklyn.core.mgmt.rebind.RebindTestFixtureWithApp; import org.apache.brooklyn.core.mgmt.rebind.RebindTestUtils; import org.apache.brooklyn.core.mgmt.rebind.RecordingRebindExceptionHandler; import org.apache.brooklyn.core.mgmt.rebind.transformer.CompoundTransformer; import org.apache.brooklyn.core.test.entity.TestApplication; import org.apache.brooklyn.util.guava.SerializablePredicate; import org.apache.brooklyn.util.os.Os; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.Test; import com.google.common.base.Objects; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; @SuppressWarnings("serial") public class CompoundTransformerTest extends RebindTestFixtureWithApp { private static final Logger LOG = LoggerFactory.getLogger(CompoundTransformerTest.class); private static String NEWLINE = Os.LINE_SEPARATOR; private File newMementoDir; @AfterMethod(alwaysRun=true) @Override public void tearDown() throws Exception { super.tearDown(); if (newMementoDir != null) FileBasedObjectStore.deleteCompletely(mementoDir); } @Test public void testXmlReplaceItemText() throws Exception { CompoundTransformer transformer = CompoundTransformer.builder() .xmlReplaceItem("Tag1/text()[.='foo']", "bar") .build(); assertSingleXmlTransformation(transformer, "<Tag1>foo</Tag1>", "<Tag1>bar</Tag1>"); assertSingleXmlTransformation(transformer, "<Tag1>baz</Tag1>", "<Tag1>baz</Tag1>"); assertSingleXmlTransformation(transformer, "<Tag2>foo</Tag2>", "<Tag2>foo</Tag2>"); // works when nested assertSingleXmlTransformation(transformer, "<Tag0><Tag1>foo</Tag1><Tag2/></Tag0>", "<Tag0><Tag1>bar</Tag1><Tag2/></Tag0>"); // keeps attributes and other children assertSingleXmlTransformation(transformer, "<Tag1 attr=\"value\">foo</Tag1>", "<Tag1 attr=\"value\">bar</Tag1>"); assertSingleXmlTransformation(transformer, "<Tag1>foo<Tag2/></Tag1>", "<Tag1>bar<Tag2/></Tag1>"); } @Test public void testXmlReplaceItemTree() throws Exception { CompoundTransformer transformer = CompoundTransformer.builder() .xmlReplaceItem("Tag1[text()='foo']", "<Tag1>bar</Tag1>") .build(); assertSingleXmlTransformation(transformer, "<Tag1>foo</Tag1>", "<Tag1>bar</Tag1>"); assertSingleXmlTransformation(transformer, "<Tag1>baz</Tag1>", "<Tag1>baz</Tag1>"); assertSingleXmlTransformation(transformer, "<Tag2>foo</Tag2>", "<Tag2>foo</Tag2>"); // works when nested assertSingleXmlTransformation(transformer, "<Tag0><Tag1>foo</Tag1><Tag2/></Tag0>", "<Tag0><Tag1>bar</Tag1><Tag2/></Tag0>"); // this deletes attributes and other children assertSingleXmlTransformation(transformer, "<Tag1 attr=\"value\">foo</Tag1>", "<Tag1>bar</Tag1>"); assertSingleXmlTransformation(transformer, "<Tag1>foo<Tag2/></Tag1>", "<Tag1>bar</Tag1>"); } @Test public void testXmlReplaceItemAttribute() throws Exception { // note, the syntax for changing an attribute value is obscure, especially the RHS CompoundTransformer transformer = CompoundTransformer.builder() .xmlReplaceItem("Tag1/@attr[.='foo']", "<xsl:attribute name=\"attr\">bar</xsl:attribute>") .build(); assertSingleXmlTransformation(transformer, "<Tag1 attr=\"foo\">foo</Tag1>", "<Tag1 attr=\"bar\">foo</Tag1>"); assertSingleXmlTransformation(transformer, "<Tag1 attr=\"baz\">foo</Tag1>", "<Tag1 attr=\"baz\">foo</Tag1>"); } @Test public void testXmlRenameTag() throws Exception { CompoundTransformer transformer = CompoundTransformer.builder() .xmlRenameTag("Tag1[text()='foo']", "Tag2") .build(); assertSingleXmlTransformation(transformer, "<Tag1>foo</Tag1>", "<Tag2>foo</Tag2>"); assertSingleXmlTransformation(transformer, "<Tag1>baz</Tag1>", "<Tag1>baz</Tag1>"); assertSingleXmlTransformation(transformer, "<Tag2>foo</Tag2>", "<Tag2>foo</Tag2>"); // works when nested assertSingleXmlTransformation(transformer, "<Tag0><Tag1>foo</Tag1><Tag2/></Tag0>", "<Tag0><Tag2>foo</Tag2><Tag2/></Tag0>"); // keeps attributes and other children assertSingleXmlTransformation(transformer, "<Tag1 attr=\"value\">foo</Tag1>", "<Tag2 attr=\"value\">foo</Tag2>"); assertSingleXmlTransformation(transformer, "<Tag1>foo<Tag2/></Tag1>", "<Tag2>foo<Tag2/></Tag2>"); } @Test public void testXmlReplaceItemActuallyAlsoRenamingTag() throws Exception { CompoundTransformer transformer = CompoundTransformer.builder() .xmlReplaceItem("Tag1[text()='foo']", "<Tag2><xsl:apply-templates select=\"@*|node()\" /></Tag2>") .build(); assertSingleXmlTransformation(transformer, "<Tag1>foo</Tag1>", "<Tag2>foo</Tag2>"); assertSingleXmlTransformation(transformer, "<Tag1>baz</Tag1>", "<Tag1>baz</Tag1>"); assertSingleXmlTransformation(transformer, "<Tag2>foo</Tag2>", "<Tag2>foo</Tag2>"); // works when nested assertSingleXmlTransformation(transformer, "<Tag0><Tag1>foo</Tag1><Tag2/></Tag0>", "<Tag0><Tag2>foo</Tag2><Tag2/></Tag0>"); // keeps attributes and other children assertSingleXmlTransformation(transformer, "<Tag1 attr=\"value\">foo</Tag1>", "<Tag2 attr=\"value\">foo</Tag2>"); assertSingleXmlTransformation(transformer, "<Tag1>foo<Tag2/></Tag1>", "<Tag2>foo<Tag2/></Tag2>"); } protected void assertSingleXmlTransformation(CompoundTransformer transformer, String xmlIn, String xmlOutExpected) throws Exception { String xmlOutActual = Iterables.getOnlyElement( transformer.getRawDataTransformers().get(BrooklynObjectType.ENTITY) ).transform(xmlIn); Assert.assertEquals(xmlOutActual, xmlOutExpected); } protected void assertXmlTransformation(CompoundTransformer transformer, String xmlIn, String xmlOutExpected) throws Exception { BrooklynMementoRawData rawData = BrooklynMementoRawData.builder() .entity("test", xmlIn) .build(); BrooklynMementoRawData rawDataOut = transformer.transform(rawData); Assert.assertEquals(rawDataOut.getEntities().get("test"), xmlOutExpected); } @Test public void testNoopTransformation() throws Exception { CompoundTransformer transformer = CompoundTransformer.builder() .build(); newApp = transformAndRebind(transformer); // Assert has expected config/fields assertEquals(newApp.getId(), origApp.getId()); } @Test public void testRenameClass() throws Exception { ConfigKey<Object> CONF1 = new BasicConfigKey<Object>(Object.class, "test.conf1"); origApp.config().set(CONF1, new OrigType("myfieldval")); CompoundTransformer transformer = CompoundTransformer.builder() .renameClassTag(OrigType.class.getName(), RenamedType.class.getName()) .build(); newApp = transformAndRebind(transformer); Object newConfVal = newApp.getConfig(CONF1); assertEquals(((RenamedType)newConfVal).myfield, "myfieldval"); } @Test public void testRenameAnonymousInnerClass() throws Exception { ConfigKey<Object> CONF1 = new BasicConfigKey<Object>(Object.class, "test.conf1"); Predicate<Entity> origPredicate = idEqualTo(origApp.getId()); origApp.config().set(CONF1, origPredicate); CompoundTransformer transformer = CompoundTransformer.builder() .renameClassTag(origPredicate.getClass().getName(), RenamedIdEqualToPredicate.class.getName()) .renameField(RenamedIdEqualToPredicate.class.getName(), "val$paramVal", "val") .build(); newApp = transformAndRebind(transformer); RenamedIdEqualToPredicate newPredicate = (RenamedIdEqualToPredicate) newApp.getConfig(CONF1); assertTrue(newPredicate.apply(newApp)); } @Test public void testRenameTypeInXml() throws Exception { CompoundTransformer transformer = CompoundTransformer.builder() .renameType("mytype.Before", "mytype.After") .build(); String input = "<entity myattrib=\"myval\">"+NEWLINE+ " <type myattrib2=\"myval2\">mytype.Before</type>"+NEWLINE+ " <nested>"+NEWLINE+ " <type myattrib3=\"myval3\">doesNotMatch</type>"+NEWLINE+ " <type myattrib4=\"myval4\">partial.mytype.Before</type>"+NEWLINE+ " <type myattrib5=\"myval5\">mytype.Before</type>"+NEWLINE+ " </nested>"+NEWLINE+ " <id>myid</id>"+NEWLINE+ "</entity>"; String expected = "<entity myattrib=\"myval\">"+NEWLINE+ " <type myattrib2=\"myval2\">mytype.After</type>"+NEWLINE+ " <nested>"+NEWLINE+ " <type myattrib3=\"myval3\">doesNotMatch</type>"+NEWLINE+ " <type myattrib4=\"myval4\">partial.mytype.Before</type>"+NEWLINE+ " <type myattrib5=\"myval5\">mytype.After</type>"+NEWLINE+ " </nested>"+NEWLINE+ " <id>myid</id>"+NEWLINE+ "</entity>"; assertSingleXmlTransformation(transformer, input, expected); } @Test public void testRenameFieldInXml() throws Exception { CompoundTransformer transformer = CompoundTransformer.builder() .renameField("MyClass", "myFieldBefore", "myFieldAfter") .build(); String input = "<entity myattrib=\"myval\">"+NEWLINE+ " <type myattrib2=\"myval2\">mytype.Before</type>"+NEWLINE+ " <config>"+NEWLINE+ " <test.conf1>"+NEWLINE+ " <MyClass>"+NEWLINE+ " <myFieldBefore class=\"string\">myfieldval</myFieldBefore>"+NEWLINE+ " </MyClass>"+NEWLINE+ " </test.conf1>"+NEWLINE+ " <test.conf2>"+NEWLINE+ " <MyOtherClass>"+NEWLINE+ " <myFieldBefore class=\"string\">myfieldval</myFieldBefore>"+NEWLINE+ " </MyOtherClass>"+NEWLINE+ " </test.conf2>"+NEWLINE+ " </config>"+NEWLINE+ "</entity>"; String expected = "<entity myattrib=\"myval\">"+NEWLINE+ " <type myattrib2=\"myval2\">mytype.Before</type>"+NEWLINE+ " <config>"+NEWLINE+ " <test.conf1>"+NEWLINE+ " <MyClass>"+NEWLINE+ " <myFieldAfter class=\"string\">myfieldval</myFieldAfter>"+NEWLINE+ " </MyClass>"+NEWLINE+ " </test.conf1>"+NEWLINE+ " <test.conf2>"+NEWLINE+ " <MyOtherClass>"+NEWLINE+ " <myFieldBefore class=\"string\">myfieldval</myFieldBefore>"+NEWLINE+ " </MyOtherClass>"+NEWLINE+ " </test.conf2>"+NEWLINE+ " </config>"+NEWLINE+ "</entity>"; assertSingleXmlTransformation(transformer, input, expected); } @Test public void testRenameClassTagInXml() throws Exception { CompoundTransformer transformer = CompoundTransformer.builder() .renameClassTag("MyClassBefore", "MyClassAfter") .build(); String input = "<entity myattrib=\"myval\">"+NEWLINE+ " <type myattrib2=\"myval2\">mytype.Before</type>"+NEWLINE+ " <config>"+NEWLINE+ " <test.conf1>"+NEWLINE+ " <MyClassBefore>"+NEWLINE+ " </MyClassBefore>"+NEWLINE+ " </test.conf1>"+NEWLINE+ " </config>"+NEWLINE+ "</entity>"; String expected = "<entity myattrib=\"myval\">"+NEWLINE+ " <type myattrib2=\"myval2\">mytype.Before</type>"+NEWLINE+ " <config>"+NEWLINE+ " <test.conf1>"+NEWLINE+ " <MyClassAfter>"+NEWLINE+ " </MyClassAfter>"+NEWLINE+ " </test.conf1>"+NEWLINE+ " </config>"+NEWLINE+ "</entity>"; assertSingleXmlTransformation(transformer, input, expected); } @Test public void testRenameClassInXml() throws Exception { CompoundTransformer transformer = CompoundTransformer.builder() .renameClass("MyClassBefore", "MyClassAfter") .build(); String input = "<entity myattrib=\"myval\">"+NEWLINE+ " <type myattrib2=\"myval2\">MyClassBefore</type>"+NEWLINE+ " <config>"+NEWLINE+ " <test.conf1 class=\"MyClassBefore\">"+NEWLINE+ " <MyClassBefore>"+NEWLINE+ " </MyClassBefore>"+NEWLINE+ " </test.conf1>"+NEWLINE+ " </config>"+NEWLINE+ "</entity>"; String expected = "<entity myattrib=\"myval\">"+NEWLINE+ " <type myattrib2=\"myval2\">MyClassAfter</type>"+NEWLINE+ " <config>"+NEWLINE+ " <test.conf1 class=\"MyClassAfter\">"+NEWLINE+ " <MyClassAfter>"+NEWLINE+ " </MyClassAfter>"+NEWLINE+ " </test.conf1>"+NEWLINE+ " </config>"+NEWLINE+ "</entity>"; assertXmlTransformation(transformer, input, expected); } @Test public void testChangeCatalogItemIdExplicitVersionInXml() throws Exception { CompoundTransformer transformer = CompoundTransformer.builder() .changeCatalogItemId("foo", "1.0", "bar", "2.0") .build(); String input = "<entity myattrib=\"myval\">"+NEWLINE+ " <catalogItemId>foo:1.0</catalogItemId>"+NEWLINE+ " <config>ignore</config>"+NEWLINE+ "</entity>"; String expected = "<entity myattrib=\"myval\">"+NEWLINE+ " <catalogItemId>bar:2.0</catalogItemId>"+NEWLINE+ " <config>ignore</config>"+NEWLINE+ "</entity>"; assertSingleXmlTransformation(transformer, input, expected); } @Test public void testChangeCatalogItemIdExplicitVersionNonMatchInXml() throws Exception { CompoundTransformer transformer = CompoundTransformer.builder() .changeCatalogItemId("foo", "1.0", "bar", "2.0") .build(); String input = "<entity myattrib=\"myval\">"+NEWLINE+ " <catalogItemId>foo:1.1</catalogItemId>"+NEWLINE+ " <config>ignore</config>"+NEWLINE+ "</entity>"; String expected = "<entity myattrib=\"myval\">"+NEWLINE+ " <catalogItemId>foo:1.1</catalogItemId>"+NEWLINE+ " <config>ignore</config>"+NEWLINE+ "</entity>"; assertSingleXmlTransformation(transformer, input, expected); } @Test public void testChangeCatalogItemIdAnyVersionInXml() throws Exception { CompoundTransformer transformer = CompoundTransformer.builder() .changeCatalogItemId("foo", "bar", "2.0") .build(); String input = "<entity myattrib=\"myval\">"+NEWLINE+ " <catalogItemId>foo:1.2</catalogItemId>"+NEWLINE+ " <config>ignore</config>"+NEWLINE+ "</entity>"; String expected = "<entity myattrib=\"myval\">"+NEWLINE+ " <catalogItemId>bar:2.0</catalogItemId>"+NEWLINE+ " <config>ignore</config>"+NEWLINE+ "</entity>"; assertSingleXmlTransformation(transformer, input, expected); } protected TestApplication transformAndRebind(CompoundTransformer transformer) throws Exception { RebindTestUtils.waitForPersisted(origApp); BrooklynMementoRawData newRawData = transform(origManagementContext, transformer); newMementoDir = persist(newRawData); return rebind(newMementoDir); } protected BrooklynMementoRawData transform(ManagementContext mgmt, CompoundTransformer transformer) throws Exception { BrooklynMementoPersisterToObjectStore reader = (BrooklynMementoPersisterToObjectStore) mgmt.getRebindManager().getPersister(); RebindExceptionHandler exceptionHandler = new RecordingRebindExceptionHandler(RebindFailureMode.FAIL_FAST, RebindFailureMode.FAIL_FAST); BrooklynMementoRawData result = transformer.transform(reader, exceptionHandler); LOG.info("Test "+getClass()+" transformed persisted state"); return result; } protected File persist(BrooklynMementoRawData rawData) throws Exception { File newMementoDir = Os.newTempDir(getClass()); FileBasedObjectStore objectStore = new FileBasedObjectStore(newMementoDir); objectStore.injectManagementContext(origManagementContext); objectStore.prepareForSharedUse(PersistMode.CLEAN, HighAvailabilityMode.DISABLED); BrooklynMementoPersisterToObjectStore persister = new BrooklynMementoPersisterToObjectStore( objectStore, ((ManagementContextInternal)origManagementContext).getBrooklynProperties(), origManagementContext.getCatalog().getRootClassLoader()); persister.enableWriteAccess(); PersistenceExceptionHandler exceptionHandler = PersistenceExceptionHandlerImpl.builder().build(); persister.checkpoint(rawData, exceptionHandler); LOG.info("Test "+getClass()+" persisted raw data to "+newMementoDir); return newMementoDir; } protected TestApplication rebind(File newMementoDir) throws Exception { newManagementContext = RebindTestUtils.managementContextBuilder(newMementoDir, classLoader) .forLive(useLiveManagementContext()) .buildUnstarted(); return (TestApplication) RebindTestUtils.rebind(RebindOptions.create() .newManagementContext(newManagementContext) .classLoader(classLoader) .mementoDir(newMementoDir)); } public static class OrigType { public String myfield; public OrigType(String myfield) { this.myfield = myfield; } } public static class RenamedType { public String myfield; public RenamedType(String myfield) { this.myfield = myfield; } } // Example method, similar to EntityPredicates where we want to move the annonymous inner class // to be a named inner class public static <T> Predicate<Entity> idEqualTo(final T paramVal) { return new SerializablePredicate<Entity>() { @Override public boolean apply(@Nullable Entity input) { return (input != null) && Objects.equal(input.getId(), paramVal); } }; } private static class RenamedIdEqualToPredicate implements SerializablePredicate<Entity> { private String val; @SuppressWarnings("unused") //used by renames above RenamedIdEqualToPredicate(String val) { this.val = val; } @Override public boolean apply(@Nullable Entity input) { return (input != null) && Objects.equal(input.getId(), val); } } }