/******************************************************************************* | |
* Copyright (c) 2006, 2010 Soyatec (http://www.soyatec.com) and others. | |
* All rights reserved. This program and the accompanying materials | |
* are made available under the terms of the Eclipse Public License v1.0 | |
* which accompanies this distribution, and is available at | |
* http://www.eclipse.org/legal/epl-v10.html | |
* | |
* Contributors: | |
* Soyatec - initial API and implementation | |
*******************************************************************************/ | |
package org.eclipse.xwt.internal.xml; | |
import java.text.MessageFormat; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import java.util.NoSuchElementException; | |
import java.util.Set; | |
import java.util.Stack; | |
import java.util.StringTokenizer; | |
import org.eclipse.xwt.IConstants; | |
import org.eclipse.xwt.XWT; | |
import org.xml.sax.Attributes; | |
import org.xml.sax.ContentHandler; | |
import org.xml.sax.Locator; | |
import org.xml.sax.SAXException; | |
import org.xml.sax.ext.Locator2; | |
import org.xml.sax.helpers.DefaultHandler; | |
/** | |
* XAML-DOM handler. | |
* | |
* @author yyang | |
* @version 1.0 | |
*/ | |
class ElementHandler extends DefaultHandler implements ContentHandler { | |
private Stack<StringBuffer> bufferStack = new Stack<StringBuffer>(); | |
private static final String[] BINDING_PROPERTIES = new String[] { | |
"path", "validationRule", "converter" | |
}; | |
/** | |
* Temporary element hierarchy | |
*/ | |
private Stack<DocumentObject> elementStack = new Stack<DocumentObject>(); | |
private Stack<Integer> elementIndexStack; | |
private Stack<Boolean> switchStack; | |
private StringBuffer textBuffer = null; | |
private Locator locator; | |
private boolean ignoreSystemProcession; | |
private boolean hasSetDefaultEncoding; | |
private ElementManager elementManager; | |
private String defaultNamespace; | |
private ExtensionParser extensionParser = new ExtensionParser(); | |
private HashMap<String, String> namespaceMapping = new HashMap<String, String>(); | |
private boolean needNormalizeName = true; | |
/** | |
* Parse the XAML extension Markup: {StaticResource test} {StaticResource RessourceKey=test} {DynamicResource {x:Static SystemColors.ControlBrushKey}} | |
* | |
* @author yyang | |
* | |
*/ | |
private class ExtensionParser { | |
private Element root; | |
private DocumentObject current; | |
public void parse(DocumentObject element, String text) { | |
if (root != null) { | |
ExtensionParser parser = new ExtensionParser(); | |
parser.parse(element, text); | |
return; | |
} | |
StringTokenizer stringTokenizer = new StringTokenizer(text, "{}", true); | |
String previous = null; | |
String nextPrevious = null; | |
while (stringTokenizer.hasMoreTokens()) { | |
String token = stringTokenizer.nextToken(); | |
if (previous != null) { | |
if (previous.equals("{")) { | |
if (token.equals("}")) { | |
// escape sequence | |
if (stringTokenizer.hasMoreTokens()) { | |
handleBlock(stringTokenizer.nextToken(" \t\n\r\f"), false); | |
} | |
} else { | |
startBlock(); | |
} | |
} else if (previous.equals("}")) { | |
endBlock(); | |
} else { | |
StringBuilder builder = new StringBuilder(); | |
builder.append(previous); | |
if (token.equals("{")) { | |
int level = 1; | |
builder.append(token); | |
while (stringTokenizer.hasMoreTokens() && level >= 0) { | |
String value = stringTokenizer.nextToken(); | |
if (value.equals("{")) { | |
level++; | |
} else if (value.equals("}")) { | |
level--; | |
} | |
if (level >= 0) { | |
builder.append(value); | |
} | |
} | |
} | |
handleBlock(builder.toString(), (nextPrevious == null || !nextPrevious.equals("}"))); | |
} | |
} | |
nextPrevious = previous; | |
previous = token; | |
} | |
element.addChild(root); | |
root = null; | |
current = null; | |
} | |
protected void startBlock() { | |
} | |
protected void endBlock() { | |
if (current != null) { | |
current = current.getParent().getParent(); | |
} | |
} | |
protected Element createElement(String token) { | |
int index = token.indexOf(':'); | |
String namespace = null; | |
String name = token; | |
if (index != -1) { | |
String prefix = token.substring(0, index); | |
name = token.substring(index + 1); | |
namespace = namespaceMapping.get(prefix); | |
} | |
if (namespace == null) { | |
namespace = defaultNamespace; | |
} | |
Element element = new Element(namespace, name, namespaceMapping); | |
element.setId(elementManager.generateID(name)); | |
if (current != null) { | |
current.addChild(element); | |
} else { | |
if (root == null) { | |
root = element; | |
} | |
} | |
current = element; | |
return element; | |
} | |
protected void handleBlock(String content, boolean newElement) { | |
String rootPattern = " \t\n\r\f=,"; | |
StringTokenizer tokenizer = new StringTokenizer(content, rootPattern, true); | |
String attributeName = null; | |
String attributeValue = null; | |
boolean equals = false; | |
Element element = null; | |
if (!newElement && current instanceof Element) { | |
element = (Element) current; | |
} | |
boolean skip = false; | |
String token = null; | |
while (skip || tokenizer.hasMoreTokens()) { | |
if (!skip) { | |
token = tokenizer.nextToken(rootPattern).trim(); | |
} | |
skip = false; | |
if (token.length() == 0) { | |
continue; | |
} | |
if (element == null) { | |
element = createElement(token); | |
} else { | |
if (token.equals("=")) { | |
equals = true; | |
if ("xpath".equalsIgnoreCase(attributeName)) { | |
attributeValue = tokenizer.nextToken(","); | |
} | |
continue; | |
} | |
if (token.equals(",")) { | |
if (attributeName != null) { | |
if (attributeValue != null) { | |
Attribute attribute = new Attribute(normalizeAttrNamespace(current.getNamespace()), attributeName, elementManager.generateID(current.getName())); | |
if (isExpendedProperty(attributeName) | |
&& "Binding".equalsIgnoreCase(element.getName())) { | |
attributeValue = expandNamespaces(element, attributeValue); | |
} | |
handleContent(attribute, attributeValue); | |
element.setAttribute(attribute); | |
current = attribute; | |
} else { | |
element.setContent(attributeName); | |
} | |
attributeName = null; | |
attributeValue = null; | |
equals = false; | |
} | |
} else { | |
if (attributeName == null) { | |
attributeName = token; | |
} else { | |
StringBuilder builder = new StringBuilder(); | |
builder.append(token); | |
if (token.startsWith("{")) { | |
int level = 1; | |
while (tokenizer.hasMoreTokens() && level > 0) { | |
String value = tokenizer.nextToken("{}"); | |
if (value.equals("{")) { | |
level++; | |
} else if (value.equals("}")) { | |
level--; | |
} | |
builder.append(value); | |
} | |
} | |
attributeValue = builder.toString(); | |
try { | |
token = tokenizer.nextToken(rootPattern).trim(); | |
skip = true; | |
continue; | |
} catch (NoSuchElementException e) { | |
} | |
} | |
} | |
} | |
skip = false; | |
} | |
if (equals) { | |
Attribute attribute = new Attribute(normalizeAttrNamespace(current.getNamespace()), attributeName, elementManager.generateID(current.getName())); | |
if (isExpendedProperty(attributeName) && "Binding".equalsIgnoreCase(element.getName())) { | |
attributeValue = expandNamespaces(element, attributeValue); | |
} | |
if (attributeValue != null) { | |
handleContent(attribute, attributeValue); | |
element.setAttribute(attribute); | |
} else { | |
element.setAttribute(attribute); | |
current = attribute; | |
} | |
} else if (attributeName != null) { | |
int index = attributeName.indexOf(":"); | |
if (index != -1) { | |
element = createElement(attributeName); | |
current = current.getParent(); | |
} else { | |
current.setContent(attributeName); | |
} | |
} | |
} | |
}; | |
static boolean isExpendedProperty(String name) { | |
// TODO need a generic solution for each property of Binding | |
for (String propertyName : BINDING_PROPERTIES) { | |
if (propertyName.equalsIgnoreCase(name)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
protected String expandNamespaces(DocumentObject element, String value) { | |
if (value.startsWith("{") || (value.indexOf(':') == -1)) { | |
return value; | |
} | |
int length = IConstants.XAML_CLR_NAMESPACE_PROTO.length(); | |
for (String prefix : namespaceMapping.keySet()) { | |
String namespace = namespaceMapping.get(prefix); | |
if (namespace.startsWith(IConstants.XAML_CLR_NAMESPACE_PROTO)) { | |
String packageName = namespace.substring(length); | |
value = value.replace(prefix + ":", packageName + '.'); | |
} | |
} | |
return value; | |
} | |
/** | |
* Default constructor. | |
* | |
* @param manager | |
* loader instance. | |
*/ | |
public ElementHandler(ElementManager elementManager) { | |
this.ignoreSystemProcession = false; | |
this.hasSetDefaultEncoding = false; | |
this.elementManager = elementManager; | |
} | |
/** | |
* Internal constructor. | |
* | |
* @param loader | |
* loader instance. | |
* @param parent | |
* the parent element of loading stream. | |
*/ | |
public ElementHandler(DocumentObject parent, ElementManager elementManager) { | |
if (parent != null) { | |
elementStack.push(parent); | |
bufferStack.push(textBuffer); | |
} | |
ignoreSystemProcession = true; | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.xml.sax.helpers.DefaultHandler#setDocumentLocator(org.xml.sax.Locator) | |
*/ | |
@Override | |
public void setDocumentLocator(Locator locator) { | |
this.locator = locator; | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.xml.sax.helpers.DefaultHandler#startDocument() | |
*/ | |
@Override | |
public void startDocument() throws SAXException { | |
elementIndexStack = new Stack<Integer>(); | |
elementIndexStack.push(0); | |
switchStack = new Stack<Boolean>(); | |
defaultNamespace = IConstants.XWT_NAMESPACE; | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.xml.sax.helpers.DefaultHandler#startPrefixMapping(java.lang.String, java.lang.String) | |
*/ | |
@Override | |
public void startPrefixMapping(String prefix, String uri) throws SAXException { | |
if (prefix.length() == 0 && !ignoreSystemProcession) { | |
defaultNamespace = uri; | |
} | |
namespaceMapping.put(prefix, uri); | |
} | |
public void endPrefixMapping(String prefix) throws SAXException { | |
namespaceMapping.remove(prefix); | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.xml.sax.helpers.DefaultHandler#endDocument() | |
*/ | |
@Override | |
public void endDocument() throws SAXException { | |
elementStack.clear(); | |
bufferStack.clear(); | |
switchStack.clear(); | |
switchStack = null; | |
defaultNamespace = null; | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int) | |
*/ | |
@Override | |
public void characters(char[] arg0, int arg1, int arg2) throws SAXException { | |
if (!switchStack.isEmpty() && !switchStack.peek()) { | |
return; | |
} | |
String string = new String(arg0, arg1, arg2); | |
if (string.length() > 0) { | |
if (textBuffer == null) { | |
textBuffer = new StringBuffer(string); | |
} else { | |
textBuffer.append(string); | |
} | |
} | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.xml.sax.helpers.DefaultHandler#skippedEntity(java.lang.String) | |
*/ | |
public void skippedEntity(String name) throws SAXException { | |
super.skippedEntity(name); | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes) | |
*/ | |
@Override | |
public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException { | |
// Set xml's encoding to default. | |
if (!ignoreSystemProcession && !hasSetDefaultEncoding) { | |
if (locator instanceof Locator2) { | |
Locator2 locator2 = (Locator2) locator; | |
String encoding = locator2.getEncoding(); | |
if (encoding != null && encoding.length() > 0) { | |
elementManager.setEncoding(encoding); | |
} | |
} | |
hasSetDefaultEncoding = true; | |
} | |
uri = normalizeNamespace(uri); | |
// If super condition does not allow parse children, ignore it. | |
if (!switchStack.isEmpty() && !switchStack.peek()) { | |
if (uri.equalsIgnoreCase(IConstants.XWT_NAMESPACE)) { | |
if ("Switch".equals(localName)) { | |
// If children is switch element, than set false to it. | |
switchStack.push(false); | |
} | |
} | |
return; | |
} | |
if (localName.indexOf('.') > 0) { | |
preProperty(uri, localName, attrs); | |
} else { | |
preElement(uri, localName, attrs); | |
} | |
elementIndexStack.push(0); | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String) | |
*/ | |
@Override | |
public void endElement(String uri, String localName, String qName) throws SAXException { | |
elementIndexStack.pop(); | |
if (!elementIndexStack.isEmpty()) { | |
int nextIndex = elementIndexStack.pop(); | |
elementIndexStack.push(nextIndex + 1); | |
} | |
uri = normalizeNamespace(uri); | |
// If super condition does not allow parse children, ignore it. | |
if (!switchStack.isEmpty() && !switchStack.peek()) { | |
if (uri.equalsIgnoreCase(IConstants.XWT_NAMESPACE)) { | |
if ("Switch".equals(localName)) { | |
switchStack.pop(); | |
} | |
} | |
return; | |
} | |
if (!elementStack.isEmpty()) { | |
// remove current node from stack | |
DocumentObject element = elementStack.peek(); | |
if (element instanceof Attribute) { | |
postProperty(uri, localName); | |
} else { | |
postElement(uri, localName); | |
} | |
} | |
} | |
private void preElement(String namespace, String name, Attributes attrs) throws SAXException { | |
// Process element ID | |
namespace = normalizeNamespace(namespace); | |
name = normalizeName(name); | |
if (name.equalsIgnoreCase("xdata")) { | |
needNormalizeName = false; | |
} | |
// Check the reference element. | |
String id = ElementManager.generateID(name); | |
// Check the same id elements. | |
if (elementManager.hasElement(id)) { | |
throw new SAXException(getFormattedMessage("Element already exists: ", id)); | |
} | |
// Process attributes: original, external | |
Set<Attribute> attributes = new HashSet<Attribute>(); | |
for (int i = 0, len = attrs.getLength(); i < len; i++) { | |
String attrUri = normalizeAttrNamespace(attrs.getURI(i)); | |
String attrName = attrs.getLocalName(i); | |
Attribute attribute; | |
int index = attrName.lastIndexOf('.'); | |
if (index != -1) { | |
String prefix = normalizeName(attrName.substring(0, index)); | |
String attributeName = (XWT.isXWTNamespace(attrUri) ? normalizeName(attrName.substring(index + 1)) : attrName); | |
attribute = new Attribute(attrUri, attributeName, id); | |
attribute.setNamePrefix(prefix); | |
} else { | |
attribute = new Attribute(attrUri, (XWT.isXWTNamespace(attrUri) ? normalizeName(attrName) : attrName), id); | |
} | |
handleContent(attribute, attrs.getValue(i)); | |
attributes.add(attribute); | |
} | |
// Create or reference element. | |
Element element = new Element(namespace, name, getIndexPath(), attributes, namespaceMapping); | |
element.setId(id); | |
// Add current node to stack | |
if (!elementStack.isEmpty()) { | |
elementStack.peek().addChild(element); | |
} | |
elementStack.push(element); | |
bufferStack.push(textBuffer); | |
textBuffer = null; | |
// Register to the element registry | |
elementManager.preElement(element); | |
} | |
private void postElement(String namespace, String name) throws SAXException { | |
// : The same as postReference : // | |
if (!elementStack.isEmpty()) { | |
// remove current node from stack | |
DocumentObject element = elementStack.pop(); | |
// Setting element text content | |
if (textBuffer != null) { | |
String text = textBuffer.toString().trim(); | |
if (text.length() > 0) { | |
handleContent(element, text); | |
} | |
} | |
textBuffer = bufferStack.pop(); | |
// Post the element registration. | |
elementManager.postElement((Element) element); | |
} | |
if (name.equalsIgnoreCase("xdata")) { | |
needNormalizeName = true; | |
} | |
} | |
protected void handleContent(DocumentObject element, String text) { | |
if (text.startsWith("{") && text.endsWith("}")) { | |
extensionParser.parse(element, text); | |
return; | |
} else { | |
// handle the case: <x:Array Type="ns:Type" > | |
if (IConstants.XAML_X_TYPE.equals(element.getName()) || IConstants.XAML_X_STATIC.equals(element.getName())) { | |
int index = text.indexOf(':'); | |
if (index != -1) { | |
String ns = text.substring(0, index); | |
String content = text.substring(index + 1); | |
String namespace = namespaceMapping.get(ns); | |
if (namespace != null) { | |
Element childElement = new Element(namespace, content, namespaceMapping); | |
childElement.setId(elementManager.generateID(element.getName())); | |
element.addChild(childElement); | |
return; | |
} | |
} | |
} | |
} | |
if (element instanceof Attribute | |
&& IConstants.XWT_X_NAMESPACE.equals(element.getNamespace())) { | |
if (IConstants.XAML_STYLE.equalsIgnoreCase(element.getName()) | |
|| IConstants.XAML_X_CLASS.equalsIgnoreCase(element | |
.getName()) | |
|| IConstants.XAML_X_CLASS_FACTORY.equalsIgnoreCase(element | |
.getName())) { | |
// handle the expansion of x:Style = "(j:class).variable" | |
// and handle the expansion of x:Class = "(j:class).variable" | |
text = expandNamespaces(element, text); | |
} | |
} | |
element.setContent(text); | |
} | |
/** | |
* It is compatible with XAML. | |
* <p> | |
* It must support the following features: | |
* | |
* 1. <Panel> <Button> <Button.Content>Hello World</Button.Content> </Button> </Panel> | |
* | |
* 2. <Panel> <Panel.Composite> <Button Content="Hello World" /> </Panel.Composite> </Panel> | |
* | |
* 3. <Panel> <Panel.Composite> <Button> <Button.Content>Hello World</Button.Content> </Button> </Panel.Composite> </Panel> | |
* | |
* 4. <Panel> <Panel.Composite> <Button> <Button.Content>Hello</Button.Content> </Button> <Button> <Button.Content>World</Button.Content> </Button> </Panel.Composite> </Panel> | |
* </p> | |
* | |
* @param namespace | |
* attribute namespace | |
* @param name | |
* attribute full name. like: Button.Content | |
*/ | |
private void preProperty(String namespace, String name, Attributes attrs) throws SAXException { | |
String elementTag = normalizeName(name.substring(0, name.indexOf('.'))); | |
String attributeName = (XWT.isXWTNamespace(namespace) ? normalizeName(name.substring(name.indexOf('.') + 1)) : name); | |
String elementId = null; | |
// Search the owner element and get the ID. | |
for (int i = elementStack.size() - 1; i >= 0; i--) { | |
DocumentObject element = elementStack.get(i); | |
if (element instanceof Element && element.getName().equals(elementTag)) { | |
elementId = element.getId(); | |
break; | |
} | |
} | |
if (elementId == null) { | |
preElement(namespace, name, attrs); | |
} else { | |
namespace = normalizeNamespace(namespace); | |
// Process attributes: original, external | |
Collection<Attribute> collection = new ArrayList<Attribute>(); | |
for (int i = 0, len = attrs.getLength(); i < len; i++) { | |
String attrUri = normalizeNamespace(attrs.getURI(i)); | |
String attrName = attrs.getLocalName(i); | |
Attribute attr; | |
int index = attrName.lastIndexOf('.'); | |
if (index != -1) { | |
String prefix = normalizeName(attrName.substring(0, index)); | |
String aName = (XWT.isXWTNamespace(namespace) ? normalizeName(attrName.substring(index + 1)) : attrName); | |
attr = new Attribute(attrUri, aName, elementId); | |
attr.setNamePrefix(prefix); | |
} else { | |
attr = new Attribute(attrUri, (XWT.isXWTNamespace(namespace) ? normalizeName(attrName) : attrName), elementId); | |
} | |
handleContent(attr, attrs.getValue(i)); | |
collection.add(attr); | |
} | |
Attribute attribute = new Attribute(normalizeAttrNamespace(namespace), attributeName, getIndexPath(), elementId, collection); | |
elementStack.push(attribute); | |
bufferStack.push(textBuffer); | |
textBuffer = null; | |
} | |
} | |
/** | |
* @see #preProperty(String, String) | |
*/ | |
private void postProperty(String namespace, String name) throws SAXException { | |
if (!elementStack.isEmpty()) { | |
// remove current node from stack | |
Attribute attribute = (Attribute) elementStack.pop(); | |
// Setting attribute content | |
if (textBuffer != null) { | |
String text = textBuffer.toString().trim(); | |
if (text.length() > 0) { | |
handleContent(attribute, text); | |
} | |
} | |
textBuffer = bufferStack.pop(); | |
Element element = elementManager.getElement(attribute.getId()); | |
element.setAttribute(attribute); | |
} | |
} | |
/** | |
* Normalize the namespace string. If uri is null or length is zero, returns default namespace. | |
* | |
* @param uri | |
* The namespace which to normalize. | |
* @return Returns the normalized namespace. | |
*/ | |
private String normalizeNamespace(String uri) { | |
if (uri == null || uri.length() == 0) { | |
uri = defaultNamespace; | |
} | |
if (uri.startsWith(IConstants.XAML_CLR_NAMESPACE_PROTO)) { | |
return uri; | |
} | |
return uri.toLowerCase(); | |
} | |
/** | |
* Normalize the namespace string. If uri is null or length is zero, returns default namespace. | |
* | |
* @param uri | |
* The namespace which to normalize. | |
* @return Returns the normalized namespace. | |
*/ | |
private String normalizeAttrNamespace(String uri) { | |
if (uri == null || uri.length() == 0) { | |
uri = defaultNamespace; | |
} | |
if (uri.startsWith(IConstants.XAML_CLR_NAMESPACE_PROTO)) { | |
uri = defaultNamespace; | |
} | |
return uri.toLowerCase(); | |
} | |
/** | |
* <p> | |
* Normalize the tag name to class-formal format. | |
* </p> | |
* <p> | |
* e.g. Normalize "font-face-name" to "FontFaceName" | |
* </p> | |
* | |
* @param tagName | |
* The tag name which to normalize. | |
* @return the normalized tag name. | |
*/ | |
private String normalizeName(String tagName) { | |
if (!needNormalizeName) { | |
return tagName; | |
} | |
StringBuffer buffer = new StringBuffer(); | |
boolean isH = false; | |
for (int i = 0, len = tagName.length(); i < len; i++) { | |
char c = tagName.charAt(i); | |
if (i == 0) { | |
buffer.append(Character.toUpperCase(c)); | |
} else { | |
switch (c) { | |
case '-': | |
isH = true; | |
break; | |
case '.': | |
isH = true; | |
buffer.append(c); | |
break; | |
default: | |
if (isH) { | |
buffer.append(Character.toUpperCase(c)); | |
isH = false; | |
} else { | |
buffer.append(c); | |
} | |
break; | |
} | |
} | |
} | |
return buffer.toString(); | |
} | |
/** | |
* Get attribute value in default namespace scope. | |
* | |
* @param attrs | |
* sax attributes. | |
* @param name | |
* attribute name. | |
* @return Returns attribute value. If attribute not found, returns null; | |
*/ | |
private String getDefaultAttribute(Attributes attrs, String name) { | |
String value = attrs.getValue(IConstants.XWT_NAMESPACE, name); | |
if (value == null || value.length() == 0) { | |
value = attrs.getValue(name); | |
} | |
if (value == null || value.length() == 0) { | |
return null; | |
} else { | |
return value; | |
} | |
} | |
private String getFormattedMessage(String msg, Object... args) { | |
StringBuffer buffer = new StringBuffer("[" + locator.getLineNumber() + "," + locator.getColumnNumber() + "] "); | |
buffer.append(msg); | |
return MessageFormat.format(buffer.toString(), args); | |
} | |
/** | |
* @return the '/' separated, zero based, path of current {@link Element}. | |
*/ | |
private String getIndexPath() { | |
StringBuilder sb = new StringBuilder(); | |
for (Integer index : elementIndexStack) { | |
if (sb.length() != 0) { | |
sb.append('/'); | |
} | |
sb.append(index.intValue()); | |
} | |
return sb.toString(); | |
} | |
} |