/*
* 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 com.google.common.base.Preconditions.checkNotNull;
import java.util.Collection;
import java.util.Map;
import org.apache.brooklyn.api.mgmt.rebind.RebindExceptionHandler;
import org.apache.brooklyn.api.mgmt.rebind.mementos.BrooklynMementoRawData;
import org.apache.brooklyn.api.objs.BrooklynObjectType;
import org.apache.brooklyn.core.mgmt.persist.BrooklynMementoPersisterToObjectStore;
import org.apache.brooklyn.core.mgmt.rebind.transformer.impl.XsltTransformer;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.core.text.TemplateProcessor;
import org.apache.brooklyn.util.text.Strings;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
@Beta
public class CompoundTransformer {
public static final CompoundTransformer NOOP = builder().build();
// TODO Does not yet handle BrooklynMementoTransformer, for changing an entire BrooklynMemento.
// Need to do refactoring in RebindManager and BrooklynMementoPersisterToObjectStore to convert
// from a BrooklynMementoRawData to a BrooklynMemento.
public static Builder builder() {
return new Builder();
}
public static class Builder {
private final Multimap<BrooklynObjectType, RawDataTransformer> rawDataTransformers = ArrayListMultimap.<BrooklynObjectType, RawDataTransformer>create();
public Builder rawDataTransformer(RawDataTransformer val) {
for (BrooklynObjectType type : BrooklynObjectType.values()) {
rawDataTransformer(type, val);
}
return this;
}
public Builder rawDataTransformer(BrooklynObjectType type, RawDataTransformer val) {
rawDataTransformers.put(checkNotNull(type, "type"), checkNotNull(val, "val"));
return this;
}
/** registers the given XSLT code to be applied to all persisted {@link BrooklynObjectType}s */
public Builder xsltTransformer(String xslt) {
XsltTransformer xsltTransformer = new XsltTransformer(xslt);
for (BrooklynObjectType type : BrooklynObjectType.values()) {
rawDataTransformer(type, xsltTransformer);
}
return this;
}
/** registers the given XSLT code to be applied to the given persisted {@link BrooklynObjectType}s */
public Builder xsltTransformer(BrooklynObjectType type, String xslt) {
XsltTransformer xsltTransformer = new XsltTransformer(xslt);
rawDataTransformer(type, xsltTransformer);
return this;
}
protected Builder xsltTransformerFromXsltFreemarkerTemplateUrl(String templateUrl, Map<String,String> vars) {
String xsltTemplate = ResourceUtils.create(this).getResourceAsString(templateUrl);
String xslt = TemplateProcessor.processTemplateContents(xsltTemplate, vars);
return xsltTransformer(xslt);
}
protected Builder xsltTransformerRecursiveCopyWithExtraRules(String ...rules) {
String xsltTemplate = ResourceUtils.create(this).getResourceAsString("classpath://org/apache/brooklyn/core/mgmt/rebind/transformer/recursiveCopyWithExtraRules.xslt");
String xslt = TemplateProcessor.processTemplateContents(xsltTemplate, ImmutableMap.of("extra_rules", Strings.join(rules, "\n")));
return xsltTransformer(xslt);
}
/** Discards and replaces the item at the given XPath.
* <p>
* For example to replace all occurrences
* of text "foo" inside a tag "Tag1", you can use <code>TagName/text()[.='foo']</code>;
* passing <code>bar</code> as the second argument would cause
* <code><Tag1>foo</Tag1></code> to become <code><Tag1>bar</Tag1></code>.
* <p>
* Note that java class names may require conversion prior to invoking this;
* see {@link #toXstreamClassnameFormat(String)}.
*/
// ie TagName/text()[.='foo'] with 'bar' causes <Tag1>foo</Tag1> to <Tag1>bar</Tag1>
public Builder xmlReplaceItem(String xpathToMatch, String newValue) {
return xsltTransformerRecursiveCopyWithExtraRules(
"<xsl:template match=\""+xpathToMatch+"\">"
+ newValue
+ "</xsl:template>");
}
/**
* Replaces a tag, but while continuing to recurse.
*/
public Builder xmlRenameTag(String xpathToMatch, String newValue) {
return xmlReplaceItem(xpathToMatch,
"<"+newValue+">"
+ "<xsl:apply-templates select=\"@*|node()\" />"
+ "</"+newValue+">");
}
public Builder xmlChangeAttribute(String xpathToMatch, String newValue) {
return xmlReplaceItem(xpathToMatch,
"<xsl:attribute name='{local-name()}'>" +
newValue +
"</xsl:attribute>");
}
/**
* Renames an explicit type name reference in brooklyn-xstream serialization.
* <p>
* Really this changes the contents inside any tag named "type",
* where the contents match the oldVal, they are changed to the newVal.
* <p>
* In brooklyn-xstream, the "type" node typically gives the name of a java or catalog type to be used
* when creating an instance; that's how this works.
*/
public Builder renameType(String oldVal, String newVal) {
return xmlReplaceItem("type/text()[.='"+toXstreamClassnameFormat(oldVal)+"']", toXstreamClassnameFormat(newVal));
// previously this did a more complex looping, essentially
// <when .=oldVal>newVal</when><otherwise><apply-templates/></otherwise>
// but i think these are equivalent
}
/**
* Renames an implicit class name reference (a tag).
* <p>
* Really this changes any XML tag matching a given old value;
* the tag is changed to the new value.
* <p>
* In brooklyn-xstream many tags correspond to the java class of an object;
* that's how this works to to change the java class (or xstream alias)
* of a persisted instance, included nested instances.
*/
public Builder renameClassTag(String oldVal, String newVal) {
return xmlRenameTag(toXstreamClassnameFormat(oldVal), toXstreamClassnameFormat(newVal));
}
/**
* Renames a field in xstream serialization.
* <p>
* Really this changes an XML tag inside another tag,
* where the outer tag and inner tag match the clazz and oldVal values given here,
* the inner tag is changed to the newVal.
* <p>
* In brooklyn-xstream, tags corresponding to fields are contained in the tag
* corresponding to the class name; that's how this works.
*/
public Builder renameField(String clazz, String oldVal, String newVal) {
return xmlRenameTag(toXstreamClassnameFormat(clazz)+"/"+toXstreamClassnameFormat(oldVal), toXstreamClassnameFormat(newVal));
}
/** Changes the contents of an XML tag 'catalogItemId' where the
* old text matches oldSymbolicName and optionally oldVersion
* to have newSymbolicName and newVersion.
* <p>
* This provides a programmatic way to change the catalogItemID. */
public Builder changeCatalogItemId(String oldSymbolicName, String oldVersion,
String newSymbolicName, String newVersion) {
if (oldVersion==null)
return changeCatalogItemId(oldSymbolicName, newSymbolicName, newVersion);
// warnings use underscore notation because that's what CompoundTransformerLoader uses
return xmlReplaceItem("catalogItemId/text()[.='"+
Preconditions.checkNotNull(oldSymbolicName, "old_symbolic_name")+":"+Preconditions.checkNotNull(oldVersion, "old_version")+"']",
Preconditions.checkNotNull(newSymbolicName, "new_symbolic_name")+":"+Preconditions.checkNotNull(newVersion, "new_version"));
}
/** As {@link #changeCatalogItemId(String, String, String, String)} matching any old version. */
public Builder changeCatalogItemId(String oldSymbolicName, String newSymbolicName, String newVersion) {
return xmlReplaceItem("catalogItemId/text()[starts-with(.,'"+Preconditions.checkNotNull(oldSymbolicName, "old_symbolic_name")+":')]",
Preconditions.checkNotNull(newSymbolicName, "new_symbolic_name")+":"+Preconditions.checkNotNull(newVersion, "new_version"));
}
/**
* Updates all references to a class to a new value
* @param oldName the old name of the class
* @param newName the new name of the class to be used instead
*/
public Builder renameClass(String oldName, String newName) {
return renameClassTag(oldName, newName)
.xmlChangeAttribute("//@class[.='" + oldName + "']", newName)
.renameType(oldName, newName);
//TODO update reference attributes
}
private String toXstreamClassnameFormat(String val) {
// xstream format for inner classes is like <org.apache.brooklyn.core.mgmt.rebind.transformer.CompoundTransformerTest_-OrigType>
return (val.contains("$")) ? val.replace("$", "_-") : val;
}
public CompoundTransformer build() {
return new CompoundTransformer(this);
}
}
private final Multimap<BrooklynObjectType, RawDataTransformer> rawDataTransformers;
protected CompoundTransformer(Builder builder) {
rawDataTransformers = builder.rawDataTransformers;
}
public BrooklynMementoRawData transform(BrooklynMementoPersisterToObjectStore reader, RebindExceptionHandler exceptionHandler) throws Exception {
BrooklynMementoRawData rawData = reader.loadMementoRawData(exceptionHandler);
return transform(rawData);
}
public BrooklynMementoRawData transform(BrooklynMementoRawData rawData) throws Exception {
Map<String, String> entities = MutableMap.copyOf(rawData.getEntities());
Map<String, String> locations = MutableMap.copyOf(rawData.getLocations());
Map<String, String> policies = MutableMap.copyOf(rawData.getPolicies());
Map<String, String> enrichers = MutableMap.copyOf(rawData.getEnrichers());
Map<String, String> feeds = MutableMap.copyOf(rawData.getFeeds());
Map<String, String> catalogItems = MutableMap.copyOf(rawData.getCatalogItems());
// TODO @neykov asks whether transformers should be run in registration order,
// rather than in type order. TBD. (would be an easy change.)
// (they're all strings so it shouldn't matter!)
for (BrooklynObjectType type : BrooklynObjectType.values()) {
Collection<RawDataTransformer> transformers = rawDataTransformers.get(type);
for (RawDataTransformer transformer : transformers) {
switch (type) {
case ENTITY:
for (Map.Entry<String, String> entry : entities.entrySet()) {
entry.setValue(transformer.transform(entry.getValue()));
}
break;
case LOCATION:
for (Map.Entry<String, String> entry : locations.entrySet()) {
entry.setValue(transformer.transform(entry.getValue()));
}
break;
case POLICY:
for (Map.Entry<String, String> entry : policies.entrySet()) {
entry.setValue(transformer.transform(entry.getValue()));
}
break;
case ENRICHER:
for (Map.Entry<String, String> entry : enrichers.entrySet()) {
entry.setValue(transformer.transform(entry.getValue()));
}
break;
case FEED:
for (Map.Entry<String, String> entry : feeds.entrySet()) {
entry.setValue(transformer.transform(entry.getValue()));
}
break;
case CATALOG_ITEM:
for (Map.Entry<String, String> entry : catalogItems.entrySet()) {
entry.setValue(transformer.transform(entry.getValue()));
}
break;
case UNKNOWN:
break; // no-op
default:
throw new IllegalStateException("Unexpected brooklyn object type "+type);
}
}
}
return BrooklynMementoRawData.builder()
.entities(entities)
.locations(locations)
.policies(policies)
.enrichers(enrichers)
.feeds(feeds)
.catalogItems(catalogItems)
.build();
}
@VisibleForTesting
Multimap<BrooklynObjectType, RawDataTransformer> getRawDataTransformers() {
return ArrayListMultimap.create(rawDataTransformers);
}
}