/*
* 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.geode.management.internal.configuration.domain;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.xpath.XPathExpressionException;
import org.apache.geode.internal.Assert;
import org.apache.logging.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.apache.geode.DataSerializable;
import org.apache.geode.DataSerializer;
import org.apache.geode.InternalGemFireError;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.internal.cache.xmlcache.CacheXml;
import org.apache.geode.internal.cache.xmlcache.CacheXmlGenerator;
import org.apache.geode.internal.lang.StringUtils;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.management.internal.configuration.utils.XmlUtils;
import org.apache.geode.management.internal.configuration.utils.XmlUtils.XPathContext;
/****
* Domain class for defining a GemFire entity in XML.
*
*
*/
public class XmlEntity implements DataSerializable {
private static final long serialVersionUID = 1L;
private static final Logger logger = LogService.getLogger();
private String type;
@SuppressWarnings("unused")
private String parentType;
private Map<String, String> attributes = new HashMap<String, String>();
private String xmlDefinition;
private String searchString;
private String prefix = CacheXml.PREFIX;
private String namespace = CacheXml.GEODE_NAMESPACE;
/**
* Default constructor for serialization only.
*
* @deprecated Use {@link XmlEntity#builder()}.
*/
@Deprecated
public XmlEntity() {}
/**
* Construct a new XmlEntity while creating XML from the cache using the element which has a type
* and attribute matching those given.
*
* @param type Type of the XML element to search for. Should be one of the constants from the
* {@link CacheXml} class. For example, CacheXml.REGION.
* @param key Key of the attribute to match, for example, "name" or "id".
* @param value Value of the attribute to match.
*/
public XmlEntity(final String type, final String key, final String value) {
this.type = type;
this.attributes.put(key, value);
init();
}
/****
* Construct a new XmlEntity while creating Xml from the cache using the element which has
* attributes matching those given
*
* @param parentType Parent type of the XML element to search for. Should be one of the constants
* from the {@link CacheXml} class. For example, CacheXml.REGION.
*
* @param parentKey Identifier for the parent elements such "name/id"
* @param parentValue Value of the identifier
* @param childType Child type of the XML element to search for within the parent . Should be one
* of the constants from the {@link CacheXml} class. For example, CacheXml.INDEX.
* @param childKey Identifier for the child element such as "name/id"
* @param childValue Value of the child element identifier
*/
public XmlEntity(final String parentType, final String parentKey, final String parentValue,
final String childType, final String childKey, final String childValue) {
// TODO this should be replaced with a builder.
// TODO consider parent as nested XmlEntity type.
this.parentType = parentType;
this.type = childType;
StringBuffer sb = new StringBuffer();
sb.append("//").append(prefix).append(':').append(parentType);
if (!StringUtils.isBlank(parentKey) && !StringUtils.isBlank(parentValue)) {
sb.append("[@").append(parentKey).append("='").append(parentValue).append("']");
}
sb.append("/").append(prefix).append(':').append(childType);
if (!StringUtils.isBlank(childKey) && !StringUtils.isBlank(childValue)) {
sb.append("[@").append(childKey).append("='").append(childValue).append("']");
}
this.searchString = sb.toString();
// no init();
}
/**
* Initialize new instances. Called from {@link #XmlEntity(String, String, String)} and
* {@link XmlEntityBuilder#build()}.
*
* @since GemFire 8.1
*/
private final void init() {
Assert.assertTrue(!StringUtils.isBlank(type));
Assert.assertTrue(!StringUtils.isBlank(prefix));
Assert.assertTrue(!StringUtils.isBlank(namespace));
Assert.assertTrue(attributes != null);
if (null == xmlDefinition) {
xmlDefinition = loadXmlDefinition();
}
}
/**
* Use the CacheXmlGenerator to create XML from the entity associated with the current cache.
*
* @return XML string representation of the entity.
*/
private final String loadXmlDefinition() {
final Cache cache = CacheFactory.getAnyInstance();
final StringWriter stringWriter = new StringWriter();
final PrintWriter printWriter = new PrintWriter(stringWriter);
CacheXmlGenerator.generate(cache, printWriter, true, false, false);
printWriter.close();
return loadXmlDefinition(stringWriter.toString());
}
/**
* Used supplied xmlDocument to extract the XML for the defined {@link XmlEntity}.
*
* @param xmlDocument to extract XML from.
* @return XML for {@link XmlEntity} if found, otherwise <code>null</code>.
* @since GemFire 8.1
*/
private final String loadXmlDefinition(final String xmlDocument) {
final Cache cache = CacheFactory.getAnyInstance();
try {
InputSource inputSource = new InputSource(new StringReader(xmlDocument));
return loadXmlDefinition(XmlUtils.getDocumentBuilder().parse(inputSource));
} catch (IOException | SAXException | ParserConfigurationException | XPathExpressionException
| TransformerFactoryConfigurationError | TransformerException e) {
throw new InternalGemFireError("Could not parse XML when creating XMLEntity", e);
}
}
/**
* Used supplied XML {@link Document} to extract the XML for the defined {@link XmlEntity}.
*
* @param document to extract XML from.
* @return XML for {@link XmlEntity} if found, otherwise <code>null</code>.
* @throws XPathExpressionException
* @throws TransformerException
* @throws TransformerFactoryConfigurationError
* @since GemFire 8.1
*/
private final String loadXmlDefinition(final Document document)
throws XPathExpressionException, TransformerFactoryConfigurationError, TransformerException {
final Cache cache = CacheFactory.getAnyInstance();
this.searchString = createQueryString(prefix, type, attributes);
logger.info("XmlEntity:searchString: {}", this.searchString);
if (document != null) {
XPathContext xpathContext = new XPathContext();
xpathContext.addNamespace(prefix, namespace);
// Create an XPathContext here
Node element = XmlUtils.querySingleElement(document, this.searchString, xpathContext);
// Must copy to preserve namespaces.
if (null != element) {
return XmlUtils.elementToString(element);
}
}
logger.warn("No XML definition could be found with name={} and attributes={}", type,
attributes);
return null;
}
/**
* Create an XmlPath query string from the given element name and attributes.
*
* @param element Name of the XML element to search for.
* @param attributes Attributes of the element that should match, for example "name" or "id" and
* the value they should equal. This list may be empty.
*
* @return An XmlPath query string.
*/
private String createQueryString(final String prefix, final String element,
final Map<String, String> attributes) {
StringBuilder queryStringBuilder = new StringBuilder();
Iterator<Entry<String, String>> attributeIter = attributes.entrySet().iterator();
queryStringBuilder.append("//").append(prefix).append(':').append(element);
if (attributes.size() > 0) {
queryStringBuilder.append("[");
Entry<String, String> attrEntry = attributeIter.next();
// queryStringBuilder.append("@").append(attrEntry.getKey()).append("=\"").append(attrEntry.getValue()).append("\"");
queryStringBuilder.append("@").append(attrEntry.getKey()).append("='")
.append(attrEntry.getValue()).append("'");
while (attributeIter.hasNext()) {
attrEntry = attributeIter.next();
// queryStringBuilder.append(" and
// @").append(attrEntry.getKey()).append("=\"").append(attrEntry.getValue()).append("\"");
queryStringBuilder.append(" and @").append(attrEntry.getKey()).append("='")
.append(attrEntry.getValue()).append("'");
}
queryStringBuilder.append("]");
}
return queryStringBuilder.toString();
}
public String getSearchString() {
return this.searchString;
}
public String getType() {
return this.type;
}
public Map<String, String> getAttributes() {
return this.attributes;
}
/**
* Return the value of a single attribute.
*
* @param key Key of the attribute whose while will be returned.
*
* @return The value of the attribute.
*/
public String getAttribute(String key) {
return this.attributes.get(key);
}
/**
* A convenience method to get a name or id attributes from the list of attributes if one of them
* has been set. Name takes precedence.
*
* @return The name or id attribute or null if neither is found.
*/
public String getNameOrId() {
if (this.attributes.containsKey("name")) {
return this.attributes.get("name");
}
return this.attributes.get("id");
}
public String getXmlDefinition() {
return this.xmlDefinition;
}
/**
* Gets the namespace for the element. Defaults to {@link CacheXml#GEODE_NAMESPACE} if not set.
*
* @return XML element namespace
* @since GemFire 8.1
*/
public String getNamespace() {
return namespace;
}
/**
* Gets the prefix for the element. Defaults to {@link CacheXml#PREFIX} if not set.
*
* @return XML element prefix
* @since GemFire 8.1
*/
public String getPrefix() {
return prefix;
}
@Override
public String toString() {
return "XmlEntity [namespace=" + namespace + ", type=" + this.type + ", attributes="
+ this.attributes + ", xmlDefinition=" + this.xmlDefinition + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.attributes == null) ? 0 : this.attributes.hashCode());
result = prime * result + ((this.type == null) ? 0 : this.type.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
XmlEntity other = (XmlEntity) obj;
if (this.attributes == null) {
if (other.attributes != null)
return false;
} else if (!this.attributes.equals(other.attributes))
return false;
if (this.namespace == null) {
if (other.namespace != null)
return false;
} else if (!this.namespace.equals(other.namespace))
return false;
if (this.type == null) {
if (other.type != null)
return false;
} else if (!this.type.equals(other.type))
return false;
return true;
}
@Override
public void toData(DataOutput out) throws IOException {
DataSerializer.writeString(this.type, out);
DataSerializer.writeObject(this.attributes, out);
DataSerializer.writeString(this.xmlDefinition, out);
DataSerializer.writeString(this.searchString, out);
DataSerializer.writeString(this.prefix, out);
DataSerializer.writeString(this.namespace, out);
}
@Override
public void fromData(DataInput in) throws IOException, ClassNotFoundException {
this.type = DataSerializer.readString(in);
this.attributes = DataSerializer.readObject(in);
this.xmlDefinition = DataSerializer.readString(in);
this.searchString = DataSerializer.readString(in);
this.prefix = DataSerializer.readString(in);
this.namespace = DataSerializer.readString(in);
}
/**
* Produce a new {@link XmlEntityBuilder}.
*
* @return new {@link XmlEntityBuilder}.
* @since GemFire 8.1
*/
public static final XmlEntityBuilder builder() {
return new XmlEntityBuilder();
}
/**
* Builder for {@link XmlEntity}. Default values are as described in {@link XmlEntity}.
*
*
* @since GemFire 8.1
*/
public static final class XmlEntityBuilder {
private XmlEntity xmlEntity;
/**
* Private contstructor.
*
* @since GemFire 8.1
*/
private XmlEntityBuilder() {
xmlEntity = new XmlEntity();
}
/**
* Produce an {@link XmlEntity} with the supplied values. Builder is reset after
* {@link #build()} is called. Subsequent calls will produce a new {@link XmlEntity}.
*
* You are required to at least call {@link #withType(String)}.
*
* @return {@link XmlEntity}
* @since GemFire 8.1
*/
public XmlEntity build() {
xmlEntity.init();
final XmlEntity built = xmlEntity;
xmlEntity = new XmlEntity();
return built;
}
/**
* Sets the type or element name value as returned by {@link XmlEntity#getType()}
*
* @param type Name of element type.
* @return this {@link XmlEntityBuilder}
* @since GemFire 8.1
*/
public XmlEntityBuilder withType(final String type) {
xmlEntity.type = type;
return this;
}
/**
* Sets the element prefix and namespace as returned by {@link XmlEntity#getPrefix()} and
* {@link XmlEntity#getNamespace()} respectively. Defaults are {@link CacheXml#PREFIX} and
* {@link CacheXml#GEODE_NAMESPACE} respectively.
*
* @param prefix Prefix of element
* @param namespace Namespace of element
* @return this {@link XmlEntityBuilder}
* @since GemFire 8.1
*/
public XmlEntityBuilder withNamespace(final String prefix, final String namespace) {
xmlEntity.prefix = prefix;
xmlEntity.namespace = namespace;
return this;
}
/**
* Adds an attribute for the given <code>name</code> and <code>value</code> to the attributes
* map returned by {@link XmlEntity#getAttributes()} or {@link XmlEntity#getAttribute(String)}.
*
* @param name Name of attribute to set.
* @param value Value of attribute to set.
* @return this {@link XmlEntityBuilder}
* @since GemFire 8.1
*/
public XmlEntityBuilder withAttribute(final String name, final String value) {
xmlEntity.attributes.put(name, value);
return this;
}
/**
* Replaces all attributes with the supplied attributes {@link Map}.
*
* @param attributes {@link Map} to use.
* @return this {@link XmlEntityBuilder}
* @since GemFire 8.1
*/
public XmlEntityBuilder withAttributes(final Map<String, String> attributes) {
xmlEntity.attributes = attributes;
return this;
}
/**
* Sets a config xml document source to get the entity XML Definition from as returned by
* {@link XmlEntity#getXmlDefinition()}. Defaults to current active configuration for
* {@link Cache}.
*
* <b>Should only be used for testing.</b>
*
* @param xmlDocument Config XML document.
* @return this {@link XmlEntityBuilder}
* @since GemFire 8.1
*/
public XmlEntityBuilder withConfig(final String xmlDocument) {
xmlEntity.xmlDefinition = xmlEntity.loadXmlDefinition(xmlDocument);
return this;
}
/**
* Sets a config xml document source to get the entity XML Definition from as returned by
* {@link XmlEntity#getXmlDefinition()}. Defaults to current active configuration for
* {@link Cache}.
*
* <b>Should only be used for testing.</b>
*
* @param document Config XML {@link Document}.
* @return this {@link XmlEntityBuilder}
* @throws TransformerException
* @throws TransformerFactoryConfigurationError
* @throws XPathExpressionException
* @since GemFire 8.1
*/
public XmlEntityBuilder withConfig(final Document document) throws XPathExpressionException,
TransformerFactoryConfigurationError, TransformerException {
xmlEntity.xmlDefinition = xmlEntity.loadXmlDefinition(document);
return this;
}
}
}