/**
* Copyright 2008 Google Inc.
*
* Licensed 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.waveprotocol.wave.model.document.util;
import org.waveprotocol.wave.model.document.ReadableDocument;
import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema.PermittedCharacters;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.util.Utf16Util;
import java.util.Map;
/**
* Implementation class for XmlStringBuilder, plus extension of its interface
* for converting nodes to strings using a doc view. For details,
*
* @see XmlStringBuilder
*
*/
public class XmlStringBuilderDoc<N, E extends N, T extends N> extends XmlStringBuilder {
/**
* The xml string itself
*/
private final StringBuilder builder = new StringBuilder();
/**
* The "item length" of the xml string
*/
private int length = 0;
private final ReadableDocument<N, E, T> view;
private final PermittedCharacters permittedChars;
private XmlStringBuilderDoc(ReadableDocument<N, E, T> view, PermittedCharacters permittedChars) {
this.view = view;
this.permittedChars = permittedChars;
}
/**
* Constructs empty xml
*/
public static <N, E extends N, T extends N> XmlStringBuilderDoc<N,E,T>
createEmpty(ReadableDocument<N, E, T> view) {
return new XmlStringBuilderDoc<N,E,T>(view, PermittedCharacters.ANY);
}
/**
* Constructs empty xml with restriction on PermittedCharacters
*/
public static <N, E extends N, T extends N> XmlStringBuilderDoc<N,E,T>
createEmptyWithCharConstraints(ReadableDocument<N, E, T> view, PermittedCharacters permittedChars) {
return new XmlStringBuilderDoc<N,E,T>(view, permittedChars);
}
/** {@inheritDoc} */
@Override
public void clear() {
builder.setLength(0);
length = 0;
}
/** {@inheritDoc} */
@Override
public int getLength() {
return length;
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public XmlStringBuilderDoc<N,E,T> append(XmlStringBuilder xml) {
// Note(user): The CharSequence assignment here is a workaround for some
// eclipse problem: if we pass xml.builder directly to append, eclipse's
// compiler OKs is although the method is private, because there's a
// public method append(CharSequence), and StringBuilder implements
// CharSequence. But then we get an IlelgalAccessError at runtime!
// Note that casting (vs. assigning) xml.builder cases eclipse to tell
// you the cast is unnecessary for the same faulty reason...
if (xml instanceof XmlStringBuilderDoc) {
CharSequence seq = ((XmlStringBuilderDoc<N,E,T>)xml).builder;
builder.append(seq);
} else {
builder.append(xml.toString());
}
length += xml.getLength();
return this;
}
/** {@inheritDoc} */
@Override
public XmlStringBuilderDoc<N,E,T> wrap(String tagName) {
checkValidTagName(tagName);
builder.insert(0, "<" + tagName + ">");
builder.append("</" + tagName + ">");
length += 2;
return this;
}
/** {@inheritDoc} */
@Override
public XmlStringBuilderDoc<N,E,T> wrap(String tagName, String... attribs) {
checkValidTagName(tagName);
Preconditions.checkArgument(attribs.length % 2 == 0, "Attribs must come in string pairs");
String startTag = "<" + tagName;
for (int i = 0; i < attribs.length; i += 2) {
if (attribs[i + 1] != null) {
String attrName = attribs[i];
checkValidAttributeName(attrName);
startTag += " " + attrName + "=\"" + attrEscape(attribs[i + 1]) + "\"";
}
}
startTag += ">";
builder.insert(0, startTag);
builder.append("</" + tagName + ">");
length += 2;
return this;
}
private void checkValidTagName(String tagName) {
if (!Utf16Util.isXmlName(tagName)) {
Preconditions.illegalArgument("Invalid tag name: '" + tagName + "'");
}
}
private void checkValidAttributeName(String tagName) {
if (!Utf16Util.isXmlName(tagName)) {
Preconditions.illegalArgument("Invalid attribute name: '" + tagName + "'");
}
}
/** {@inheritDoc} */
@Override
public XmlStringBuilderDoc<N,E,T> appendText(String text) {
return appendText(text, PermittedCharacters.BLIP_TEXT);
}
/** {@inheritDoc} */
@Override
public XmlStringBuilderDoc<N,E,T> appendText(String text, PermittedCharacters permittedChars) {
length += addText(permittedChars.coerceString(text));
return this;
}
/**
* Appends the "outerXML" representation of the node
* @param node
* @return self for convenience
*/
public XmlStringBuilderDoc<N,E,T> appendNode(N node) {
length += addNode(node);
return this;
}
/**
* Appends the "innerXML" representation of the element
* @param element
* @return self for convenience
*/
public XmlStringBuilderDoc<N,E,T> appendChildXmlFragment(E element) {
length += addChildXmlFragment(element);
return this;
}
/**
* Helper
* @param element
* @param selfClosing Whether this tag is self-closing
* @return Opening tag of the given element as a String.
*/
public String startTag(E element, boolean selfClosing) {
return "<" + view.getTagName(element)
+ getAttributesString(element) + (selfClosing ? "/>" : ">");
}
/**
* Helper
* @param element
* @return Closing tag of the given element as a String.
*/
public String endTag(E element) {
return "</" + view.getTagName(element) + ">";
}
/**
* TODO(user): generalise this.
* @param element
* @return true if the element may self-close
*/
private boolean isSelfClosing(E element) {
return view.getTagName(element).equals("br");
}
/**
* Worker for appendNode
* @param node
* @return item length of node
*/
private int addNode(N node) {
E element = view.asElement(node);
if (element != null) {
boolean selfClosing = isSelfClosing(element) && view.getFirstChild(element) == null;
builder.append(startTag(element, selfClosing));
int len = 2;
if (!selfClosing) {
len += addChildXmlFragment(element);
builder.append(endTag(element));
}
return len;
} else {
String data = view.getData(view.asText(node));
return addText(permittedChars.coerceString(data));
}
}
/**
* Worker
* @param text
* @return length of text
*/
private int addText(String text) {
builder.append(xmlEscape(text));
return text.length();
}
/**
* Worker
* @param element
* @return XML fragment describing children
*/
private int addChildXmlFragment(E element) {
int size = 0;
for (N n = view.getFirstChild(element); n != null; n = view.getNextSibling(n)) {
size += addNode(n);
}
return size;
}
/**
* Worker to get the attributes from the element as a string
* @param element
* @return string of attributes, e.g., " name1='value1' name1='value2'"
*/
private String getAttributesString(E element) {
// TODO(danilatos): Investigate if it's worth optimising for ContentElement
// StringMap attributes
Map<String, String> attributes = view.getAttributes(element);
String soFar = "";
for (String key : attributes.keySet()) {
soFar += " " + key + "=\"" + attrEscape(attributes.get(key)) + "\"";
}
return soFar;
}
/** Copied from Utils in util.client to avoid dependency issues */
public static String xmlEscape(String xml) {
return xml
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">");
}
/** Copied from Utils in util.client to avoid dependency issues */
public static String attrEscape(String attrValue) {
return xmlEscape(attrValue)
.replaceAll("\"", """)
.replaceAll("'", "'");
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof XmlStringBuilderDoc) {
// NOTE(danilatos): As of this writing, GWT implementation of toString() for StringBuilder
// essentially caches the result, so it's not bad for performance to call it multiple times.
// However, the JRE version creates a copy every time. The equals() method on StringBuilders
// uses referential equality, so it is not usable. Need to be careful GWT doesn't change
// their implementation to make this equals implementation here slow.
return builder.toString().equals(((XmlStringBuilderDoc)o).builder.toString()) &&
length == ((XmlStringBuilderDoc)o).length;
}
return false;
}
/** {@inheritDoc} */
@Override
public int hashCode() {
return toString().hashCode();
}
@Override
public String getXmlString() {
return builder.toString();
}
@Override
public String toString() {
return getXmlString();
}
}