blob: 427aaf679a1ea925dea0e884267bbafbdb640df9 [file] [log] [blame]
/*
* 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.commons.lang3.StringUtils;
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.DataSerializer;
import org.apache.geode.InternalGemFireError;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.internal.Assert;
import org.apache.geode.internal.VersionedDataSerializable;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.cache.xmlcache.CacheXml;
import org.apache.geode.internal.cache.xmlcache.CacheXmlGenerator;
import org.apache.geode.internal.serialization.Version;
import org.apache.geode.logging.internal.log4j.api.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 VersionedDataSerializable {
private static final long serialVersionUID = 1L;
private static final Logger logger = LogService.getLogger();
private transient volatile CacheProvider cacheProvider;
private String type;
@SuppressWarnings("unused")
private String parentType;
private Map<String, String> attributes = new HashMap<>();
private String xmlDefinition;
private String searchString;
private String prefix = CacheXml.PREFIX;
private String namespace = CacheXml.GEODE_NAMESPACE;
private String childPrefix;
private String childNamespace;
/**
* Produce a new XmlEntityBuilder.
*
* @return new XmlEntityBuilder.
* @since GemFire 8.1
*/
public static XmlEntityBuilder builder() {
return new XmlEntityBuilder();
}
private static CacheProvider createDefaultCacheProvider() {
return () -> ((InternalCache) CacheFactory.getAnyInstance())
.getCacheForProcessingClientRequests();
}
/**
* Default constructor for serialization only.
*
* @deprecated Use {@link XmlEntity#builder()}.
*/
@Deprecated
public XmlEntity() {
cacheProvider = createDefaultCacheProvider();
}
/**
* 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) {
cacheProvider = createDefaultCacheProvider();
this.type = type;
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) {
cacheProvider = createDefaultCacheProvider();
this.parentType = parentType;
type = childType;
initializeSearchString(parentKey, parentValue, prefix, childKey, childValue);
}
/**
* 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 childPrefix Namespace prefix for the child element such as "lucene"
* @param childNamespace Namespace for the child element such as
* "http://geode.apache.org/schema/lucene"
* @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 childPrefix, final String childNamespace, final String childType,
final String childKey, final String childValue) {
// Note: Do not invoke init
cacheProvider = createDefaultCacheProvider();
this.parentType = parentType;
type = childType;
this.childPrefix = childPrefix;
this.childNamespace = childNamespace;
initializeSearchString(parentKey, parentValue, childPrefix, childKey, childValue);
}
public XmlEntity(final CacheProvider cacheProvider, final String parentType,
final String childPrefix, final String childNamespace, final String childType,
final String key, final String value) {
this.cacheProvider = cacheProvider;
this.parentType = parentType;
type = childType;
prefix = childPrefix;
namespace = childNamespace;
this.childPrefix = childPrefix;
this.childNamespace = childNamespace;
attributes.put(key, value);
searchString = "//" + this.parentType + '/' + childPrefix + ':' + type;
xmlDefinition = parseXmlForDefinition();
}
private String parseXmlForDefinition() {
final Cache cache = cacheProvider.getCache();
final StringWriter stringWriter = new StringWriter();
final PrintWriter printWriter = new PrintWriter(stringWriter);
CacheXmlGenerator.generate(cache, printWriter, false, false);
printWriter.close();
InputSource inputSource = new InputSource(new StringReader(stringWriter.toString()));
try {
Document document = XmlUtils.getDocumentBuilder().parse(inputSource);
Node element = document.getElementsByTagNameNS(childNamespace, type).item(0);
if (null != element) {
return XmlUtils.elementToString(element);
}
} catch (IOException | ParserConfigurationException | RuntimeException | SAXException
| TransformerException e) {
throw new InternalGemFireError("Could not parse XML when creating XMLEntity", e);
}
logger.warn("No XML definition could be found with name={} and attributes={}", type,
attributes);
return null;
}
private void initializeSearchString(final String parentKey, final String parentValue,
final String childPrefix, final String childKey, final String childValue) {
StringBuilder sb = new StringBuilder();
sb.append("//").append(prefix).append(':').append(parentType);
if (StringUtils.isNotBlank(parentKey) && StringUtils.isNotBlank(parentValue)) {
sb.append("[@").append(parentKey).append("='").append(parentValue).append("']");
}
sb.append('/').append(childPrefix).append(':').append(type);
if (StringUtils.isNotBlank(childKey) && StringUtils.isNotBlank(childValue)) {
sb.append("[@").append(childKey).append("='").append(childValue).append("']");
}
searchString = sb.toString();
}
/**
* Initialize new instances. Called from {@link #XmlEntity(String, String, String)} and
* {@link XmlEntityBuilder#build()}.
*
* @since GemFire 8.1
*/
private void init() {
Assert.assertTrue(StringUtils.isNotBlank(type));
Assert.assertTrue(StringUtils.isNotBlank(prefix));
Assert.assertTrue(StringUtils.isNotBlank(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 String loadXmlDefinition() {
final Cache cache = cacheProvider.getCache();
final StringWriter stringWriter = new StringWriter();
final PrintWriter printWriter = new PrintWriter(stringWriter);
CacheXmlGenerator.generate(cache, printWriter, false, false);
printWriter.close();
return loadXmlDefinition(stringWriter.toString());
}
/**
* Used supplied xmlDocument to extract the XML for the defined XmlEntity.
*
* @param xmlDocument to extract XML from.
* @return XML for XmlEntity if found, otherwise {@code null}.
* @since GemFire 8.1
*/
private String loadXmlDefinition(final String xmlDocument) {
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 XmlEntity.
*
* @param document to extract XML from.
* @return XML for XmlEntity if found, otherwise {@code null}.
* @since GemFire 8.1
*/
private String loadXmlDefinition(final Document document)
throws XPathExpressionException, TransformerFactoryConfigurationError, TransformerException {
searchString = createQueryString(prefix, type, attributes);
logger.info("XmlEntity:searchString: {}", searchString);
if (document != null) {
XPathContext xpathContext = new XPathContext();
xpathContext.addNamespace(prefix, namespace);
// TODO: wrap this line with conditional
xpathContext.addNamespace(childPrefix, childNamespace);
// Create an XPathContext here
Node element = XmlUtils.querySingleElement(document, 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.isEmpty()) {
queryStringBuilder.append('[');
Entry<String, String> attrEntry = attributeIter.next();
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(']');
}
return queryStringBuilder.toString();
}
public String getSearchString() {
return searchString;
}
public String getType() {
return type;
}
public Map<String, String> getAttributes() {
return 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 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 (attributes.containsKey("name")) {
return attributes.get("name");
}
return attributes.get("id");
}
public String getXmlDefinition() {
return 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;
}
/**
* Gets the prefix for the child element.
*
* @return XML element prefix for the child element
*/
public String getChildPrefix() {
return childPrefix;
}
/**
* Gets the namespace for the child element.
*
* @return XML element namespace for the child element
*/
public String getChildNamespace() {
return childNamespace;
}
@Override
public Version[] getSerializationVersions() {
return new Version[] {Version.GEODE_1_1_1};
}
@Override
public String toString() {
return "XmlEntity [namespace=" + namespace + ", type=" + type + ", attributes="
+ attributes + ", xmlDefinition=" + xmlDefinition + ']';
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((attributes == null) ? 0 : attributes.hashCode());
result = prime * result + ((type == null) ? 0 : 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 (attributes == null) {
if (other.attributes != null)
return false;
} else if (!attributes.equals(other.attributes))
return false;
if (namespace == null) {
if (other.namespace != null)
return false;
} else if (!namespace.equals(other.namespace))
return false;
if (type == null) {
return other.type == null;
} else
return type.equals(other.type);
}
@Override
public void toData(DataOutput out) throws IOException {
toDataPre_GEODE_1_1_1_0(out);
DataSerializer.writeString(childPrefix, out);
DataSerializer.writeString(childNamespace, out);
}
public void toDataPre_GEODE_1_1_1_0(DataOutput out) throws IOException {
DataSerializer.writeString(type, out);
DataSerializer.writeObject(attributes, out);
DataSerializer.writeString(xmlDefinition, out);
DataSerializer.writeString(searchString, out);
DataSerializer.writeString(prefix, out);
DataSerializer.writeString(namespace, out);
}
@Override
public void fromData(DataInput in) throws IOException, ClassNotFoundException {
fromDataPre_GEODE_1_1_1_0(in);
childPrefix = DataSerializer.readString(in);
childNamespace = DataSerializer.readString(in);
}
public void fromDataPre_GEODE_1_1_1_0(DataInput in) throws IOException, ClassNotFoundException {
type = DataSerializer.readString(in);
attributes = DataSerializer.readObject(in);
xmlDefinition = DataSerializer.readString(in);
searchString = DataSerializer.readString(in);
prefix = DataSerializer.readString(in);
namespace = DataSerializer.readString(in);
cacheProvider = createDefaultCacheProvider();
}
/**
* Defines how XmlEntity gets a reference to the Cache.
*/
public interface CacheProvider {
InternalCache getCache();
}
/**
* Builder for XmlEntity. Default values are as described in XmlEntity.
*
* @since GemFire 8.1
*/
public static class XmlEntityBuilder {
private XmlEntity xmlEntity;
/**
* Private constructor.
*
* @since GemFire 8.1
*/
@SuppressWarnings("deprecation")
XmlEntityBuilder() {
xmlEntity = new XmlEntity();
}
/**
* Produce an XmlEntity with the supplied values. Builder is reset after #build() is called.
* Subsequent calls will produce a new XmlEntity.
*
* You are required to at least call {@link #withType(String)}.
*
* @since GemFire 8.1
*/
@SuppressWarnings("deprecation")
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 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 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 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 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 document Config XML {@link Document}.
* @return this XmlEntityBuilder
* @since GemFire 8.1
*/
public XmlEntityBuilder withConfig(final Document document) throws XPathExpressionException,
TransformerFactoryConfigurationError, TransformerException {
xmlEntity.xmlDefinition = xmlEntity.loadXmlDefinition(document);
return this;
}
}
}