package org.jboss.as.controller;
import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADDRESS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NAME;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.controller.logging.ControllerLogger;
import org.jboss.as.controller.operations.common.Util;
import org.jboss.as.controller.parsing.Element;
import org.jboss.as.controller.parsing.ParseUtils;
import org.jboss.dmr.ModelNode;
import org.jboss.staxmapper.XMLExtendedStreamReader;
import org.jboss.staxmapper.XMLExtendedStreamWriter;
/**
* A representation of a resource as needed by the XML parser.
*
* @author Tomaz Cerar
* @author Stuart Douglas
*/
public final class PersistentResourceXMLDescription {
protected final PathElement pathElement;
private final String xmlElementName;
private final String xmlWrapperElement;
private final LinkedHashMap<String, LinkedHashMap<String, AttributeDefinition>> attributesByGroup;
protected final List<PersistentResourceXMLDescription> children;
private final Map<String, AttributeDefinition> attributeElements = new HashMap<>();
private final boolean useValueAsElementName;
private final boolean noAddOperation;
private final AdditionalOperationsGenerator additionalOperationsGenerator;
private boolean flushRequired = true;
private boolean childAlreadyRead = false;
private final Map<String, AttributeParser> attributeParsers;
private final Map<String, AttributeMarshaller> attributeMarshallers;
private final boolean useElementsForGroups;
private final String namespaceURI;
private final Set<String> attributeGroups;
private final String forcedName;
private final boolean marshallDefaultValues;
//name of the attribute that is used for wildcard elements
private final String nameAttributeName;
private PersistentResourceXMLDescription(PersistentResourceXMLBuilder builder) {
this.pathElement = builder.pathElement;
this.xmlElementName = builder.xmlElementName;
this.xmlWrapperElement = builder.xmlWrapperElement;
this.useElementsForGroups = builder.useElementsForGroups;
this.attributesByGroup = new LinkedHashMap<>();
this.namespaceURI = builder.namespaceURI;
this.attributeGroups = new HashSet<>();
if (useElementsForGroups) {
// Ensure we have a map for the default group even if there are no attributes so we don't NPE later
this.attributesByGroup.put(null, new LinkedHashMap<>());
// Segregate attributes by group
for (AttributeDefinition ad : builder.attributeList) {
LinkedHashMap<String, AttributeDefinition> forGroup = this.attributesByGroup.get(ad.getAttributeGroup());
if (forGroup == null) {
forGroup = new LinkedHashMap<>();
this.attributesByGroup.put(ad.getAttributeGroup(), forGroup);
this.attributeGroups.add(ad.getAttributeGroup());
}
forGroup.put(ad.getXmlName(), ad);
AttributeParser ap = builder.attributeParsers.getOrDefault(ad.getXmlName(), ad.getParser());
if (ap != null && ap.isParseAsElement()) {
attributeElements.put(ap.getXmlName(ad), ad);
}
}
} else {
LinkedHashMap<String, AttributeDefinition> attrs = new LinkedHashMap<>();
for (AttributeDefinition ad : builder.attributeList) {
attrs.put(ad.getXmlName(), ad);
if (ad.getParser() != null && ad.getParser().isParseAsElement()) {
attributeElements.put(ad.getParser().getXmlName(ad), ad);
}
}
// Ignore attribute-group, treat all as if they are in the default group
this.attributesByGroup.put(null, attrs);
}
this.children = new ArrayList<>();
for (PersistentResourceXMLBuilder b : builder.childrenBuilders) {
this.children.add(b.build());
}
builder.children.forEach(this.children::add);
this.useValueAsElementName = builder.useValueAsElementName;
this.noAddOperation = builder.noAddOperation;
this.additionalOperationsGenerator = builder.additionalOperationsGenerator;
this.attributeParsers = builder.attributeParsers;
this.attributeMarshallers = builder.attributeMarshallers;
this.forcedName = builder.forcedName;
this.marshallDefaultValues = builder.marshallDefaultValues;
this.nameAttributeName = builder.nameAttributeName;
}
public PathElement getPathElement() {
return this.pathElement;
}
public void parse(final XMLExtendedStreamReader reader, PathAddress parentAddress, List<ModelNode> list) throws XMLStreamException {
ModelNode op = Util.createAddOperation();
boolean wildcard = getPathElement().isWildcard();
String name = parseAttributeGroups(reader, op, wildcard);
if (wildcard && name == null) {
if (forcedName != null) {
name = forcedName;
} else {
throw ControllerLogger.ROOT_LOGGER.missingRequiredAttributes(new StringBuilder(NAME), reader.getLocation());
}
}
PathElement path = wildcard ? PathElement.pathElement(getPathElement().getKey(), name) : getPathElement();
PathAddress address = parentAddress.append(path);
if (!noAddOperation) {
op.get(ADDRESS).set(address.toModelNode());
list.add(op);
}
if (additionalOperationsGenerator != null) {
additionalOperationsGenerator.additionalOperations(address, op, list);
}
if (!reader.isEndElement()) { //only parse children if we are not on end of tag already
parseChildren(reader, address, list, op);
}
}
private String parseAttributeGroups(final XMLExtendedStreamReader reader, ModelNode op, boolean wildcard) throws XMLStreamException {
String name = parseAttributes(reader, op, attributesByGroup.get(null), wildcard); //parse attributes not belonging to a group
if (!attributeGroups.isEmpty()) {
while (reader.hasNext() && reader.nextTag() != XMLStreamConstants.END_ELEMENT) {
boolean element = attributeElements.containsKey(reader.getLocalName());
//it can be a group or element attribute
if (attributeGroups.contains(reader.getLocalName()) || element) {
if (element) {
AttributeDefinition ad = attributeElements.get(reader.getLocalName());
ad.getParser().parseElement(ad, reader, op);
if (attributeGroups.contains(reader.getLocalName())) {
parseGroup(reader, op, wildcard);
} else if (reader.isEndElement() && !attributeGroups.contains(reader.getLocalName()) && !attributeElements.containsKey(reader.getLocalName())) {
childAlreadyRead = true;
break;
}
} else {
parseGroup(reader, op, wildcard);
}
} else {
//don't break, as we read all attributes, we set that child was already read so readChildren wont do .nextTag()
childAlreadyRead = true;
return name;
}
}
flushRequired = false;
}
return name;
}
private void parseGroup(XMLExtendedStreamReader reader, ModelNode op, boolean wildcard) throws XMLStreamException {
Map<String, AttributeDefinition> groupAttrs = attributesByGroup.get(reader.getLocalName());
parseAttributes(reader, op, groupAttrs, wildcard);
// Check if there are also element attributes inside a group
while (reader.hasNext() && reader.nextTag() != END_ELEMENT) {
String attrName = reader.getLocalName();
if (attributeElements.containsKey(attrName) && groupAttrs.containsKey(attrName)) {
AttributeDefinition ad = attributeElements.get(reader.getLocalName());
ad.getParser().parseElement(ad, reader, op);
} else {
throw ParseUtils.unexpectedElement(reader);
}
}
}
private String parseAttributes(final XMLExtendedStreamReader reader, ModelNode op, Map<String, AttributeDefinition> attributes, boolean wildcard) throws XMLStreamException {
String name = null;
for (int i = 0; i < reader.getAttributeCount(); i++) {
String attributeName = reader.getAttributeLocalName(i);
String value = reader.getAttributeValue(i);
if (wildcard && nameAttributeName.equals(attributeName)) {
name = value;
} else if (attributes.containsKey(attributeName)) {
AttributeDefinition def = attributes.get(attributeName);
AttributeParser parser = attributeParsers.getOrDefault(attributeName, def.getParser());
assert parser != null;
parser.parseAndSetParameter(def, value, op, reader);
} else {
Set<String> possible = new LinkedHashSet<>(attributes.keySet());
possible.add(nameAttributeName);
throw ParseUtils.unexpectedAttribute(reader, i, possible);
}
}
//only parse attribute elements here if there are no attribute groups defined
if (attributeGroups.isEmpty() && !attributeElements.isEmpty() && reader.isStartElement()) {
String originalStartElement = reader.getLocalName();
if (reader.hasNext() && reader.nextTag() != XMLStreamConstants.END_ELEMENT) {
do {
if (attributeElements.containsKey(reader.getLocalName())) {
AttributeDefinition ad = attributeElements.get(reader.getLocalName());
AttributeParser parser = attributeParsers.getOrDefault(ad.getXmlName(), ad.getParser());
parser.parseElement(ad, reader, op);
} else {
return name; //this means we only have children left, return so child handling logic can take over
}
childAlreadyRead = true;
} while (!reader.getLocalName().equals(originalStartElement) && reader.hasNext() && reader.nextTag() != XMLStreamConstants.END_ELEMENT);
}
}
return name;
}
private Map<String, PersistentResourceXMLDescription> getChildrenMap() {
Map<String, PersistentResourceXMLDescription> res = new HashMap<>();
for (PersistentResourceXMLDescription child : children) {
if (child.xmlWrapperElement != null) {
res.put(child.xmlWrapperElement, child);
} else {
res.put(child.xmlElementName, child);
}
}
return res;
}
private void parseChildren(final XMLExtendedStreamReader reader, PathAddress parentAddress, List<ModelNode> list, ModelNode op) throws XMLStreamException {
if (children.size() == 0) {
if (flushRequired && attributeGroups.isEmpty() && attributeElements.isEmpty()) {
ParseUtils.requireNoContent(reader);
}
if (childAlreadyRead) {
throw ParseUtils.unexpectedElement(reader);
}
} else {
Map<String, PersistentResourceXMLDescription> children = getChildrenMap();
if (childAlreadyRead || (reader.hasNext() && reader.nextTag() != XMLStreamConstants.END_ELEMENT)) {
do {
PersistentResourceXMLDescription child = children.get(reader.getLocalName());
if (child != null) {
if (child.xmlWrapperElement != null) {
if (reader.getLocalName().equals(child.xmlWrapperElement)) {
if (reader.hasNext() && reader.nextTag() == END_ELEMENT) { return; }
} else {
throw ParseUtils.unexpectedElement(reader);
}
child.parse(reader, parentAddress, list);
while (reader.nextTag() != END_ELEMENT && !reader.getLocalName().equals(child.xmlWrapperElement)) {
child.parse(reader, parentAddress, list);
}
} else {
child.parse(reader, parentAddress, list);
}
} else if (attributeElements.containsKey(reader.getLocalName())) {
AttributeDefinition ad = attributeElements.get(reader.getLocalName());
ad.getParser().parseElement(ad, reader, op);
} else {
throw ParseUtils.unexpectedElement(reader, children.keySet());
}
} while (reader.hasNext() && reader.nextTag() != XMLStreamConstants.END_ELEMENT);
}
}
}
public void persist(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
persist(writer, model, namespaceURI);
}
private void writeStartElement(XMLExtendedStreamWriter writer, String namespaceURI, String localName) throws XMLStreamException {
if (namespaceURI != null) {
writer.writeStartElement(namespaceURI, localName);
} else {
writer.writeStartElement(localName);
}
}
private void startSubsystemElement(XMLExtendedStreamWriter writer, String namespaceURI, boolean empty) throws XMLStreamException {
if (writer.getNamespaceContext().getPrefix(namespaceURI) == null) {
// Unknown namespace; it becomes default
writer.setDefaultNamespace(namespaceURI);
if (empty) {
writer.writeEmptyElement(Element.SUBSYSTEM.getLocalName());
} else {
writer.writeStartElement(Element.SUBSYSTEM.getLocalName());
}
writer.writeNamespace(null, namespaceURI);
} else {
if (empty) {
writer.writeEmptyElement(namespaceURI, Element.SUBSYSTEM.getLocalName());
} else {
writer.writeStartElement(namespaceURI, Element.SUBSYSTEM.getLocalName());
}
}
}
public void persist(XMLExtendedStreamWriter writer, ModelNode model, String namespaceURI) throws XMLStreamException {
boolean wildcard = getPathElement().isWildcard();
model = wildcard ? model.get(getPathElement().getKey()) : model.get(getPathElement().getKeyValuePair());
boolean isSubsystem = getPathElement().getKey().equals(ModelDescriptionConstants.SUBSYSTEM);
if (!isSubsystem && !model.isDefined() && !useValueAsElementName) {
return;
}
boolean writeWrapper = xmlWrapperElement != null;
if (writeWrapper) {
writeStartElement(writer, namespaceURI, xmlWrapperElement);
}
if (wildcard) {
for (String name : model.keys()) {
ModelNode subModel = model.get(name);
if (useValueAsElementName) {
writeStartElement(writer, namespaceURI, name);
} else {
writeStartElement(writer, namespaceURI, xmlElementName);
writer.writeAttribute(nameAttributeName, name);
}
persistAttributes(writer, subModel);
persistChildren(writer, subModel);
writer.writeEndElement();
}
} else {
final boolean empty = attributeGroups.isEmpty() && children.isEmpty();
if (useValueAsElementName) {
writeStartElement(writer, namespaceURI, getPathElement().getValue());
} else if (isSubsystem) {
startSubsystemElement(writer, namespaceURI, empty);
} else {
writeStartElement(writer, namespaceURI, xmlElementName);
}
persistAttributes(writer, model);
persistChildren(writer, model);
// Do not attempt to write end element if the <subsystem/> has no elements!
if (!isSubsystem || !empty) {
writer.writeEndElement();
}
}
if (writeWrapper) {
writer.writeEndElement();
}
}
private void persistAttributes(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
marshallAttributes(writer, model, attributesByGroup.get(null).values(), null);
if (useElementsForGroups) {
for (Map.Entry<String, LinkedHashMap<String, AttributeDefinition>> entry : attributesByGroup.entrySet()) {
if (entry.getKey() == null) {
continue;
}
marshallAttributes(writer, model, entry.getValue().values(), entry.getKey());
}
}
}
private void marshallAttributes(XMLExtendedStreamWriter writer, ModelNode model, Collection<AttributeDefinition> attributes, String group) throws XMLStreamException {
boolean started = false;
//we sort attributes to make sure that attributes that marshall to elements are last
List<AttributeDefinition> sortedAds = new ArrayList<>(attributes.size());
List<AttributeDefinition> elementAds = null;
for (AttributeDefinition ad : attributes) {
if (ad.getParser().isParseAsElement()) {
if (elementAds == null) {
elementAds = new ArrayList<>();
}
elementAds.add(ad);
} else {
sortedAds.add(ad);
}
}
if (elementAds != null) {
sortedAds.addAll(elementAds);
}
for (AttributeDefinition ad : sortedAds) {
AttributeMarshaller marshaller = attributeMarshallers.getOrDefault(ad.getXmlName(), ad.getAttributeMarshaller());
if (marshaller.isMarshallable(ad, model, marshallDefaultValues)) {
if (!started && group != null) {
if (elementAds != null) {
writer.writeStartElement(group);
} else {
writer.writeEmptyElement(group);
}
started = true;
}
marshaller.marshall(ad, model, marshallDefaultValues, writer);
}
}
if (elementAds != null && started) {
writer.writeEndElement();
}
}
public void persistChildren(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
for (PersistentResourceXMLDescription child : children) {
child.persist(writer, model);
}
}
/**
* @param resource resource for which path we are creating builder
* @return PersistentResourceXMLBuilder
* @deprecated please use {@linkplain PersistentResourceXMLBuilder(PathElement, String)} variant
*/
@SuppressWarnings("deprecation")
@Deprecated
public static PersistentResourceXMLBuilder builder(PersistentResourceDefinition resource) {
return new PersistentResourceXMLBuilder(resource.getPathElement());
}
/**
* @param resource resource for which path we are creating builder
* @return PersistentResourceXMLBuilder
* @deprecated please use {@linkplain PersistentResourceXMLBuilder(PathElement, String)} variant
*/
@SuppressWarnings("deprecation")
@Deprecated
public static PersistentResourceXMLBuilder builder(ResourceDefinition resource) {
return new PersistentResourceXMLBuilder(resource.getPathElement());
}
/**
*
* @param resource resource for which path we are creating builder
* @param namespaceURI xml namespace to use for this resource, usually used for top level elements such as subsystems
* @return PersistentResourceXMLBuilder
* @deprecated please use {@linkplain PersistentResourceXMLBuilder(PathElement, String)} variant
*/
@SuppressWarnings("deprecation")
@Deprecated
public static PersistentResourceXMLBuilder builder(PersistentResourceDefinition resource, String namespaceURI) {
return new PersistentResourceXMLBuilder(resource.getPathElement(), namespaceURI);
}
/**
* Creates builder for passed path element
* @param pathElement for which we are creating builder
* @return PersistentResourceXMLBuilder
*/
public static PersistentResourceXMLBuilder builder(final PathElement pathElement) {
return new PersistentResourceXMLBuilder(pathElement);
}
/**
* Creates builder for passed path element
*
* @param pathElement for which we are creating builder
* @param namespaceURI xml namespace to use for this resource, usually used for top level elements such as subsystems
* @return PersistentResourceXMLBuilder
*/
public static PersistentResourceXMLBuilder builder(final PathElement pathElement, final String namespaceURI) {
return new PersistentResourceXMLBuilder(pathElement, namespaceURI);
}
public static final class PersistentResourceXMLBuilder {
protected final PathElement pathElement;
private final String namespaceURI;
private String xmlElementName;
private String xmlWrapperElement;
private boolean useValueAsElementName;
private boolean noAddOperation;
private AdditionalOperationsGenerator additionalOperationsGenerator;
private final LinkedList<AttributeDefinition> attributeList = new LinkedList<>();
private final List<PersistentResourceXMLBuilder> childrenBuilders = new ArrayList<>();
private final List<PersistentResourceXMLDescription> children = new ArrayList<>();
private final LinkedHashMap<String, AttributeParser> attributeParsers = new LinkedHashMap<>();
private final LinkedHashMap<String, AttributeMarshaller> attributeMarshallers = new LinkedHashMap<>();
private boolean useElementsForGroups = true;
private String forcedName;
private boolean marshallDefaultValues = false;
private String nameAttributeName = NAME;
private PersistentResourceXMLBuilder(final PathElement pathElement) {
this.pathElement = pathElement;
this.namespaceURI = null;
this.xmlElementName = pathElement.isWildcard() ? pathElement.getKey() : pathElement.getValue();
}
private PersistentResourceXMLBuilder(final PathElement pathElement, String namespaceURI) {
this.pathElement = pathElement;
this.namespaceURI = namespaceURI;
this.xmlElementName = pathElement.isWildcard() ? pathElement.getKey() : pathElement.getValue();
}
public PersistentResourceXMLBuilder addChild(PersistentResourceXMLBuilder builder) {
this.childrenBuilders.add(builder);
return this;
}
public PersistentResourceXMLBuilder addChild(PersistentResourceXMLDescription builder) {
this.children.add(builder);
return this;
}
public PersistentResourceXMLBuilder addAttribute(AttributeDefinition attribute) {
this.attributeList.add(attribute);
return this;
}
public PersistentResourceXMLBuilder addAttribute(AttributeDefinition attribute, AttributeParser attributeParser) {
this.attributeList.add(attribute);
this.attributeParsers.put(attribute.getXmlName(), attributeParser);
return this;
}
public PersistentResourceXMLBuilder addAttribute(AttributeDefinition attribute, AttributeParser attributeParser, AttributeMarshaller attributeMarshaller) {
this.attributeList.add(attribute);
this.attributeParsers.put(attribute.getXmlName(), attributeParser);
this.attributeMarshallers.put(attribute.getXmlName(), attributeMarshaller);
return this;
}
public PersistentResourceXMLBuilder addAttributes(AttributeDefinition... attributes) {
Collections.addAll(this.attributeList, attributes);
return this;
}
public PersistentResourceXMLBuilder setXmlWrapperElement(final String xmlWrapperElement) {
this.xmlWrapperElement = xmlWrapperElement;
return this;
}
public PersistentResourceXMLBuilder setXmlElementName(final String xmlElementName) {
this.xmlElementName = xmlElementName;
return this;
}
public PersistentResourceXMLBuilder setUseValueAsElementName(final boolean useValueAsElementName) {
this.useValueAsElementName = useValueAsElementName;
return this;
}
public PersistentResourceXMLBuilder setNoAddOperation(final boolean noAddOperation) {
this.noAddOperation = noAddOperation;
return this;
}
public PersistentResourceXMLBuilder setAdditionalOperationsGenerator(final AdditionalOperationsGenerator additionalOperationsGenerator) {
this.additionalOperationsGenerator = additionalOperationsGenerator;
return this;
}
/**
* This method permit to set a forced name for resource created by parser.
* This is useful when xml tag haven't an attribute defining the name for the resource,
* but the tag name itself is sufficient to decide the name for the resource
* For example when you have 2 different tag of the same xsd type representing same resource with different name
*
* @param forcedName the name to be forced as resourceName
* @return the PersistentResourceXMLBuilder itself
*/
public PersistentResourceXMLBuilder setForcedName(String forcedName) {
this.forcedName = forcedName;
return this;
}
/**
* Sets whether attributes with an {@link org.jboss.as.controller.AttributeDefinition#getAttributeGroup attribute group}
* defined should be persisted to a child element whose name is the name of the group. Child elements
* will be ordered based on the order in which attributes are added to this builder. Child elements for
* attribute groups will be ordered before elements for child resources.
*
* @param useElementsForGroups {@code true} if child elements should be used.
* @return a builder that can be used for further configuration or to build the xml description
*/
public PersistentResourceXMLBuilder setUseElementsForGroups(boolean useElementsForGroups) {
this.useElementsForGroups = useElementsForGroups;
return this;
}
/**
* If set to false, default attribute values won't be persisted
*
* @param marshallDefault weather default values should be persisted or not.
* @return builder
*/
public PersistentResourceXMLBuilder setMarshallDefaultValues(boolean marshallDefault) {
this.marshallDefaultValues = marshallDefault;
return this;
}
/**
* Sets name for "name" attribute that is used for wildcard resources.
* It defines name of attribute one resource xml element to be used for such identifier
* If not set it defaults to "name"
*
* @param nameAttributeName xml attribute name to be used for resource name
* @return builder
*/
public PersistentResourceXMLBuilder setNameAttributeName(String nameAttributeName) {
this.nameAttributeName = nameAttributeName;
return this;
}
public PersistentResourceXMLDescription build() {
return new PersistentResourceXMLDescription(this);
}
}
/**
* Some resources require more operations that just a simple add. This interface provides a hook for these to be plugged in.
*/
public interface AdditionalOperationsGenerator {
/**
* Generates any additional operations required by the resource
*
* @param address The address of the resource
* @param addOperation The add operation for the resource
* @param operations The operation list
*/
void additionalOperations(final PathAddress address, final ModelNode addOperation, final List<ModelNode> operations);
}
}