package org.dcache.services.info.serialisation; import org.springframework.beans.factory.annotation.Required; import java.util.Map; import org.dcache.services.info.base.BooleanStateValue; import org.dcache.services.info.base.FloatingPointStateValue; import org.dcache.services.info.base.IntegerStateValue; import org.dcache.services.info.base.State; import org.dcache.services.info.base.StateExhibitor; import org.dcache.services.info.base.StatePath; import org.dcache.services.info.base.StringStateValue; /** * This serialiser maps the dCache state directly into an XML InfoSet. * <p> * For the most part, this is a simple mapping with some support for handling * branch-nodes with a known special parent branch differently. * <p> * NB, instances of this Class are not thread-safe: the caller is responsible for * ensuring no concurrent calls to serialise(). * * @author Paul Millar <paul.millar@desy.de> */ public class XmlSerialiser extends SubtreeVisitor implements StateSerialiser { public static final String NAME = "xml"; /** The types used within the XML structure */ private static final String _newline = "\n"; private static final String _xmlns = "http://www.dcache.org/2008/01/Info"; private StringBuilder _out; private int _indentationLevel; private String _indentationPrefix = ""; private boolean _isTopBranch; private StatePath _lastBranchPath; private String _lastBranchElementName; private String _lastBranchIdName; private boolean _haveLastBranch; private StateExhibitor _exhibitor; @Required public void setStateExhibitor(StateExhibitor exhibitor) { _exhibitor = exhibitor; } private static class Attribute { final String name, value; Attribute(String iName, String iValue) { name = iName; value = iValue; } } /** * Serialise the current dCache state into XML; * @return a String containing dCache current state as XML data. */ @Override public String serialise() { return serialise(null); } /** * Serialise the current dCache state into XML, starting at the given path. This * selects only a subset of the total available XML infoset, but the resulting document * will validate. * @param start the StatePath to start serialising data. * @return a String containing dCache current state as XML data. */ @Override public String serialise(StatePath start) { _out = new StringBuilder(); _isTopBranch = true; _haveLastBranch = false; _indentationLevel = 0; updateIndentPrefix(); if (start != null) { setVisitScopeToSubtree(start); } else { setVisitScopeToEverything(); } addElement("<?xml version=\"1.0\"?>"); _exhibitor.visitState(this); /** * We ensure that there is always at least one element (the <dCache/> element). * _isTopBranch is true only if no state has been traversed, so no <dCache> element * emitted. */ if (_isTopBranch) { _haveLastBranch = true; _lastBranchElementName = getBranchLabel(null); emitLastBeginElement(true); } return _out.toString(); } @Override public String getName() { return NAME; } /* Deal with branch movement */ @Override public void visitCompositePreDescend(StatePath path, Map<String,String> metadata) { enteringBranch(path, metadata); } @Override public void visitCompositePostDescend(StatePath path, Map<String,String> metadata) { exitingBranch(path, metadata); } /* Deal with metric values */ @Override public void visitInteger(StatePath path, IntegerStateValue value) { emitLastBeginElement(false); addElement(buildMetricElement(path.getLastElement(), value.getTypeName(), value.toString())); } @Override public void visitString(StatePath path, StringStateValue value) { emitLastBeginElement(false); addElement(buildMetricElement(path.getLastElement(), value.getTypeName(), xmlTextMarkup(value.toString()))); } @Override public void visitBoolean(StatePath path, BooleanStateValue value) { emitLastBeginElement(false); addElement(buildMetricElement(path.getLastElement(), value.getTypeName(), value.toString())); } @Override public void visitFloatingPoint(StatePath path, FloatingPointStateValue value) { emitLastBeginElement(false); addElement(buildMetricElement(path.getLastElement(), value.getTypeName(), value.toString())); } /** * Provide all appropriate activity when entering a new branch. * <p> * When dealing with lists, we use the branch metadata: * <ul> * <li> METADATA_BRANCH_CLASS_KEY is the name of the list item class (e.g., * for items under the dCache.pools branch, this is "pool") * <li> METADATA_BRANCH_IDNAME_KEY is the name of identifier (e.g., "name") * </ul> * <p> * We mostly push information onto a (single item) stack so we can * emit empty branches like: * <pre> * <branchname attr1="value1" /> * </pre> * * @param path The path of the new branch * @param metadata The keyword-value pairs for this branch. */ private void enteringBranch(StatePath path, Map<String,String> metadata) { emitLastBeginElement(false); /* Build info and store it */ _lastBranchPath = path; String branchClass = null; if (metadata != null) { branchClass = metadata.get(State.METADATA_BRANCH_CLASS_KEY); } if (branchClass != null) { _lastBranchElementName = branchClass; _lastBranchIdName = metadata.get(State.METADATA_BRANCH_IDNAME_KEY); } else { _lastBranchElementName = getBranchLabel(path); _lastBranchIdName = null; } _haveLastBranch = true; } /** * Method for handling the generic case when iterating out of a branch. * @param path * @param metadata */ private void exitingBranch(StatePath path, Map<String,String> metadata) { if (_haveLastBranch && ((path == null && _lastBranchPath == null) || (path != null && path.equals(_lastBranchPath))) ) { emitLastBeginElement(true); return; } emitLastBeginElement(false); // this should be a no-op: we should have no last-branch to emit. _indentationLevel--; updateIndentPrefix(); String branchClass = metadata != null ? metadata.get(State.METADATA_BRANCH_CLASS_KEY) : null; String label = branchClass != null ? branchClass : getBranchLabel(path); addElement(endElement(label)); } /** * emit XML for the previous branch. If the previous element was not a branch then * this method does nothing. */ private void emitLastBeginElement(boolean isEmpty) { if (!_haveLastBranch) { return; } _haveLastBranch = false; Attribute[] attrs = null; if (_isTopBranch) { attrs = new Attribute[1]; attrs[0] = new Attribute("xmlns", _xmlns); _isTopBranch = false; } else { if (_lastBranchIdName != null) { attrs = new Attribute[1]; attrs[0] = new Attribute(_lastBranchIdName, getBranchLabel(_lastBranchPath)); } } addElement(beginElement(_lastBranchElementName, attrs, isEmpty)); if (!isEmpty) { _indentationLevel++; updateIndentPrefix(); } } /** * Add an element to the output stream with correct indentation. * @param element the text (element, PI, ...) to add. */ private void addElement(String element) { _out.append(_indentationPrefix); _out.append(element); _out.append(_newline); } /** * Build an XML metric element based on information. * @param name the name of the metric * @param type the type of the metric * @param value the value */ private String buildMetricElement(String name, String type, String value) { StringBuilder sb = new StringBuilder(); Attribute attr[] = new Attribute[2]; attr[0] = new Attribute("name", name); attr[1] = new Attribute("type", type); sb.append(beginElement("metric", attr, false)); sb.append(value); sb.append(endElement("metric")); return sb.toString(); } /** * Build a String that opens an element * @param name the element's name * @param attr either an array of attributes for this element, or null. * @param isEmpty whether the element contains no data. * @return a String representing the start of this element */ private String beginElement(String name, Attribute[] attr, boolean isEmpty) { StringBuilder sb = new StringBuilder(); sb.append("<").append(name); if (attr != null) { for (Attribute anAttr : attr) { sb.append(" "); sb.append(anAttr.name); sb.append("=\""); sb.append(xmlTextMarkup(anAttr.value)); sb.append("\""); } } if (isEmpty) { sb.append("/"); } sb.append(">"); return sb.toString(); } /** * Build a string that closes an element * @param name the name of the element to open * @return a String. */ private String endElement(String name) { return "</"+name+">"; } /** * Mark-up an String so it can be included as XML data. Specifically, we * mark-up any occurrences of '<', '&', '>', '\"' and '\''. * * @param value the string value to mark-up * @return value that is safe to include in as an XML text-node. */ private String xmlTextMarkup(String value) { return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("\'", "'"); } /** * Update our stored prefix for indentation. */ private void updateIndentPrefix() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < _indentationLevel; i++) { sb.append(" "); } _indentationPrefix = sb.toString(); } /** * Return the suitable label to use for this branch * @param path the StatePath under consideration * @return the label for this branch. */ private String getBranchLabel(StatePath path) { return path != null ? path.getLastElement() : "dCache"; } }