package com.anjlab.eclipse.tapestry5.hyperlink;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.UserDataHandler;
import com.anjlab.eclipse.tapestry5.hyperlink.XmlFragment.XmlAttributeName;
import com.anjlab.eclipse.tapestry5.hyperlink.XmlFragment.XmlAttributeValue;
import com.anjlab.eclipse.tapestry5.hyperlink.XmlFragment.XmlTag;
public abstract class XmlFragment
{
protected final IDocument document;
protected final IRegion region;
public XmlFragment(IDocument document, IRegion region)
{
this.document = document;
this.region = region;
}
private String toString;
@Override
public String toString()
{
if (toString == null)
{
try
{
StringBuilder builder = new StringBuilder();
builder.append(getClass().getSimpleName())
.append(" [region(offset: ")
.append(region == null ? "null" : region.getOffset())
.append(", length: ")
.append(region == null ? "null" : region.getLength())
.append(")]: ")
.append(region == null ? "null" : document.get(region.getOffset(), region.getLength()));
toString = builder.toString();
}
catch (BadLocationException e)
{
toString = super.toString();
}
}
return toString;
}
public static class XmlContextFragment extends XmlFragment
{
public XmlContextFragment(IDocument document, IRegion region)
{
super(document, region);
}
public XmlAtomicFragment getFragmentAt(int offset)
{
XmlTag currentTag = getCurrentTag();
return currentTag == null
? null
: currentTag.getFragmentAt(offset);
}
private Optional<XmlTag> rootTag;
private XmlTag getRootTag()
{
if (rootTag == null)
{
try
{
IRegion tagRegion = findRootTagRegion(document);
rootTag = new Optional<XmlFragment.XmlTag>(
tagRegion == null
? null
: new XmlTag(this, tagRegion));
}
catch (BadLocationException e)
{
rootTag = new Optional<XmlFragment.XmlTag>(null);
}
}
return rootTag.value;
}
private Optional<XmlTag> currentTag;
public XmlTag getCurrentTag()
{
if (currentTag == null)
{
try
{
IRegion tagRegion = findTagRegion(document, region.getOffset());
currentTag = new Optional<XmlFragment.XmlTag>(
tagRegion == null
? null
: new XmlTag(this, tagRegion));
}
catch (BadLocationException e)
{
currentTag = new Optional<XmlFragment.XmlTag>(null);
}
}
return currentTag.value;
}
private static IRegion findTagRegion(IDocument document, int offset) throws BadLocationException
{
int leftIndex = offset;
int rightIndex = offset + 1;
boolean foundTagStart = false;
for (; leftIndex >= 0; leftIndex--)
{
char ch = document.getChar(leftIndex);
if (ch == '<')
{
foundTagStart = true;
break;
}
if (ch == '>')
{
// We're in tag's body
return null;
}
}
if (!foundTagStart)
{
// We can't do anything here, because we don't even know if we're in a tag
return null;
}
for (; rightIndex < document.getLength(); rightIndex++)
{
char ch = document.getChar(rightIndex);
if (ch == '>')
{
return new Region(leftIndex, rightIndex - leftIndex + 1);
}
if (ch == '<')
{
return new Region(leftIndex, rightIndex - leftIndex);
}
}
return null;
}
private IRegion findRootTagRegion(IDocument document) throws BadLocationException
{
// Find the beginning of the root XML tag - skip everything before first tag definition
boolean inComment = false;
for (int i = 0; i < document.getLength(); i++)
{
if (inComment)
{
if (document.getChar(i) == '-')
{
if (i + "->".length() < document.getLength() && "->".equals(document.get(i + 1, "->".length())))
{
inComment = false;
i += "->".length();
}
}
}
else if (document.getChar(i) == '<')
{
if (i + 1 < document.getLength() && isValidFirstCharForTagDefinition(document.getChar(i + 1)))
{
return findTagRegion(document, i);
}
else if (i + "!--".length() < document.getLength() && "!--".equals(document.get(i + 1, "!--".length())))
{
inComment = true;
i += "!--".length();
}
}
}
return null;
}
}
public static class XmlAtomicFragment extends XmlFragment
{
public final XmlTag xmlTag;
public final Optional<String> value;
public XmlAtomicFragment(XmlTag xmlTag, String value, int offset)
{
super(xmlTag.document, new Region(offset, StringUtils.isEmpty(value) ? 0 : value.length()));
this.xmlTag = xmlTag;
this.value = new Optional<String>(value);
}
public boolean hasValue()
{
return value.hasValue();
}
private FQName fqName;
public FQName getFQName()
{
if (fqName == null)
{
fqName = FQName.parse(value);
}
return fqName;
}
}
public static class XmlTagName extends XmlAtomicFragment
{
public XmlTagName(XmlTag xmlTag, String value, int offset)
{
super(xmlTag, value, offset);
}
}
public static class XmlAttributeName extends XmlAtomicFragment
{
public XmlAttributeName(XmlTag xmlTag, String value, int offset)
{
super(xmlTag, value, offset);
}
}
public static class XmlAttributeValue extends XmlAtomicFragment
{
public final XmlAttributeName attributeName;
public XmlAttributeValue(XmlTag xmlTag, String value, int offset, XmlAttributeName attributeName)
{
super(xmlTag, value, offset);
this.attributeName = attributeName;
}
}
public static class FQName
{
public final String prefix;
public final String name;
public FQName(String prefix, String name)
{
this.prefix = prefix;
this.name = name;
}
@Override
public String toString()
{
return "{" + prefix + ":" + name + "}";
}
public static FQName parse(Optional<String> value)
{
if (!value.hasValue())
{
return new FQName(null, null);
}
int colonIndex = value.value.indexOf(':');
return colonIndex < 0
? new FQName(null, value.value)
: new FQName(value.value.substring(0, colonIndex),
colonIndex + 1 < value.value.length() ? value.value.substring(colonIndex + 1) : null);
}
}
public static class Optional<T>
{
public final T value;
public Optional(T value)
{
this.value = value;
}
public boolean hasValue()
{
return value != null;
}
@Override
public boolean equals(Object obj)
{
if (obj instanceof XmlFragment.Optional)
{
if (value == null)
{
return this == obj;
}
return ObjectUtils.equals(value, ((XmlFragment.Optional<?>) obj).value);
}
return false;
}
@Override
public int hashCode()
{
return value == null
? super.hashCode()
: value.hashCode();
}
}
public static class XmlTag extends XmlFragment
{
private XmlTagName tagName;
private boolean selfClosing;
private boolean closing;
private boolean comment;
private final Map<XmlAttributeName, XmlAttributeValue> attributes;
public XmlAtomicFragment getFragmentAt(int offset)
{
if (tagName != null && inRegion(tagName.region, offset))
{
return tagName;
}
for (Entry<XmlAttributeName, XmlAttributeValue> attr : attributes.entrySet())
{
if (inRegion(attr.getKey().region, offset))
{
return attr.getKey();
}
if (inRegion(attr.getValue().region, offset))
{
return attr.getValue();
}
}
return null;
}
public String resolveNamespacePrefix(String prefix)
{
if (StringUtils.isEmpty(prefix))
{
return null;
}
for (Entry<XmlAttributeName, XmlAttributeValue> attribute : attributes.entrySet())
{
if ("xmlns".equals(attribute.getKey().getFQName().prefix)
&& prefix.equals(attribute.getKey().getFQName().name))
{
return attribute.getValue().value.value;
}
}
XmlTag rootTag = contextFragment.getRootTag();
if (rootTag != null && rootTag != this)
{
return rootTag.resolveNamespacePrefix(prefix);
}
return null;
}
public boolean isClosing()
{
return closing;
}
public boolean isComment()
{
return comment;
}
public boolean isSelfClosing()
{
return selfClosing;
}
private enum ParsingContext
{
TAG_NAME, ATTRIBUTE_NAME, ATTRIBUTE_VALUE, EQUAL_SIGN
}
public final XmlContextFragment contextFragment;
public XmlTag(XmlContextFragment contextFragment, IRegion tagRegion) throws BadLocationException
{
super(contextFragment.document, tagRegion);
this.contextFragment = contextFragment;
this.attributes = new HashMap<XmlAttributeName, XmlAttributeValue>();
parse(document, tagRegion);
}
private class ParserState
{
private XmlTag.ParsingContext parsingContext = ParsingContext.TAG_NAME;
private XmlAttributeName attributeName = null;
private boolean valueInQuotes = false;
private char quoteChar = 0;
private int startOffset = -1;
private int offset = -1;
private StringBuilder builder = new StringBuilder();
public ParserState(int startOffset)
{
this.startOffset = startOffset;
this.offset = startOffset - 1;
}
private void waitForAttributeName()
{
builder.setLength(0);
parsingContext = ParsingContext.ATTRIBUTE_NAME;
valueInQuotes = false;
startOffset = -1;
}
private void waitForAttributeValue()
{
builder.setLength(0);
parsingContext = ParsingContext.ATTRIBUTE_VALUE;
valueInQuotes = false;
startOffset = -1;
}
private void waitForEqualSign()
{
builder.setLength(0);
parsingContext = ParsingContext.EQUAL_SIGN;
valueInQuotes = false;
startOffset = -1;
}
private void waitForAttributeValueInQuotes(char quoteChar)
{
parsingContext = ParsingContext.ATTRIBUTE_VALUE;
valueInQuotes = true;
this.quoteChar = quoteChar;
startOffset = -1;
}
public void pushChar(char ch)
{
offset++;
if (parsingContext == ParsingContext.TAG_NAME)
{
if (!isValidCharForFQName(ch))
{
tagName = new XmlTagName(XmlTag.this, builder.toString(), startOffset);
waitForAttributeName();
}
else
{
builder.append(ch);
}
}
else if (parsingContext == ParsingContext.ATTRIBUTE_NAME)
{
if (!isValidCharForFQName(ch))
{
if (startOffset != -1)
{
attributeName = new XmlAttributeName(XmlTag.this, builder.toString(), startOffset);
if (ch == '=')
{
waitForAttributeValue();
}
else
{
waitForEqualSign();
}
}
else if (ch == '\'' || ch == '"')
{
newAttributeValueStartedWithoutAttributeName(ch);
}
else
{
// There's nothing there in current buffer, just ignore this char as whitespace
}
}
else
{
rememberChar(ch);
}
}
else if (parsingContext == ParsingContext.EQUAL_SIGN)
{
if (ch == '=')
{
waitForAttributeValue();
}
else if (Character.isWhitespace(ch))
{
// Skip white spaces before =
}
else if (isValidCharForFQName(ch))
{
newAttributeNameStarted(ch);
}
else if (ch == '\'' || ch == '"')
{
newAttributeValueStartedWithoutAttributeName(ch);
}
}
else if (parsingContext == ParsingContext.ATTRIBUTE_VALUE)
{
if (valueInQuotes)
{
if (quoteChar == ch)
{
addAttribute(XmlTag.this, attributeName, builder, startOffset);
waitForAttributeName();
}
else
{
rememberChar(ch);
}
}
else if (ch == '\'' || ch == '"')
{
// Begin looking for attribute value
valueInQuotes = true;
quoteChar = ch;
}
else if (Character.isWhitespace(ch))
{
// Skip whitespaces after =
}
else if (isValidCharForFQName(ch))
{
newAttributeNameStarted(ch);
}
}
}
private void newAttributeValueStartedWithoutAttributeName(char ch)
{
// Attribute value started without attribute name
attributeName = null;
waitForAttributeValueInQuotes(ch);
}
private void newAttributeNameStarted(char ch)
{
// New attribute name started
// Current attribute name has no corresponding attribute value
// Add current attribute
addAttribute(XmlTag.this, attributeName, builder, startOffset);
waitForAttributeName();
rememberChar(ch);
}
private void rememberChar(char ch)
{
if (startOffset == -1)
{
startOffset = offset;
}
builder.append(ch);
}
}
private void parse(IDocument document, IRegion tagRegion)
{
try
{
// Skip leading and trailing </ and />
int leftIndex = tagRegion.getOffset();
int rightIndex = tagRegion.getOffset() + tagRegion.getLength() - 1;
if (document.getChar(leftIndex) == '<')
{
leftIndex++;
if (document.getChar(leftIndex) == '/')
{
leftIndex++;
closing = true;
}
else
{
String commentStart = "!--";
if (leftIndex + commentStart.length() < document.getLength())
{
if (commentStart.equals(document.get(leftIndex, commentStart.length())))
{
comment = true;
return;
}
}
}
}
if (document.getChar(rightIndex) == '>')
{
rightIndex--;
if (document.getChar(rightIndex) == '/')
{
rightIndex--;
selfClosing = true;
}
}
ParserState state = new ParserState(leftIndex);
for (int i = leftIndex; i <= rightIndex ; i++)
{
char ch = document.getChar(i);
state.pushChar(ch);
}
// Flush buffers
state.pushChar((char) 0);
}
catch (BadLocationException e)
{
// Ignore
}
}
private void addAttribute(XmlTag xmlTag, XmlAttributeName attributeName, StringBuilder value, int offset)
{
if (attributeName == null)
{
attributeName = new XmlAttributeName(xmlTag, null, -1);
}
attributes.put(attributeName,
new XmlAttributeValue(xmlTag, value.toString(), offset, attributeName));
}
public Map<XmlAttributeName, XmlAttributeValue> attributes()
{
return attributes;
}
public FQName getFQName()
{
return tagName == null
? new FQName(null, null)
: tagName.getFQName();
}
}
private static boolean isValidFirstCharForTagDefinition(char ch)
{
return Character.isLetter(ch) || ch == '_' || ch == ':';
}
public static boolean inRegion(IRegion region, int offset)
{
return region != null
&& region.getOffset() <= offset
&& offset < region.getOffset() + region.getLength();
}
// http://pic.dhe.ibm.com/infocenter/wci/v6r0m0/index.jsp?topic=%2Fcom.ibm.websphere.cast_iron.doc%2Fref_Valid_Node_Names.html
private static final String INVALID_CHARS_FOR_SIMPLE_NAME = ":~/\\;?$&%@^=*+()|'\"`{}[]<>";
private static boolean isValidCharForFQName(char ch)
{
return ch == ':'
|| (!Character.isWhitespace(ch)
&& INVALID_CHARS_FOR_SIMPLE_NAME.indexOf(ch) < 0
&& (Character.isLetterOrDigit(ch) || ch == '_' || ch == '-' || ch == '.'));
}
}
class XmlTagNodeAdapter extends BaseNodeAdapter
{
private XmlTag xmlTag;
public XmlTagNodeAdapter(XmlTag xmlTag)
{
this.xmlTag = xmlTag;
}
@Override
public NamedNodeMap getAttributes()
{
final List<Entry<XmlAttributeName, XmlAttributeValue>> attributes =
new ArrayList<Map.Entry<XmlAttributeName,XmlAttributeValue>>(xmlTag.attributes().entrySet());
return new NamedNodeMap()
{
@Override
public int getLength()
{
return attributes.size();
}
@Override
public Node item(int index)
{
final Entry<XmlAttributeName, XmlAttributeValue> attribute = attributes.get(index);
return new BaseNodeAdapter()
{
@Override
public String getNamespaceURI()
{
return xmlTag.resolveNamespacePrefix(attribute.getKey().getFQName().prefix);
}
@Override
public String getLocalName()
{
return attribute.getKey().getFQName().name;
}
@Override
public String getNodeValue() throws DOMException
{
return attribute.getValue().value.value;
}
@Override
public short getNodeType()
{
return Node.ATTRIBUTE_NODE;
}
};
}
@Override
public Node setNamedItemNS(Node arg) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public Node setNamedItem(Node arg) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public Node removeNamedItemNS(String namespaceURI, String localName) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public Node removeNamedItem(String name) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public Node getNamedItemNS(String namespaceURI, String localName) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public Node getNamedItem(String name)
{
throw new UnsupportedOperationException();
}
};
}
@Override
public String getLocalName()
{
return xmlTag.getFQName().name;
}
@Override
public String getNamespaceURI()
{
String prefix = xmlTag.getFQName().prefix;
return xmlTag.resolveNamespacePrefix(prefix);
}
@Override
public String lookupNamespaceURI(String prefix)
{
return xmlTag.resolveNamespacePrefix(prefix);
}
@Override
public short getNodeType()
{
return Node.ELEMENT_NODE;
}
@Override
public String getPrefix()
{
return xmlTag.getFQName().prefix;
}
@Override
public boolean hasAttributes()
{
return !xmlTag.attributes().isEmpty();
}
}
class BaseNodeAdapter implements Node
{
@Override
public Node appendChild(Node newChild) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public Node cloneNode(boolean deep)
{
throw new UnsupportedOperationException();
}
@Override
public short compareDocumentPosition(Node other) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public String getBaseURI()
{
throw new UnsupportedOperationException();
}
@Override
public NodeList getChildNodes()
{
throw new UnsupportedOperationException();
}
@Override
public Object getFeature(String feature, String version)
{
throw new UnsupportedOperationException();
}
@Override
public Node getFirstChild()
{
throw new UnsupportedOperationException();
}
@Override
public Node getLastChild()
{
throw new UnsupportedOperationException();
}
@Override
public Node getNextSibling()
{
throw new UnsupportedOperationException();
}
@Override
public String getNodeName()
{
throw new UnsupportedOperationException();
}
@Override
public String getNodeValue() throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public Document getOwnerDocument()
{
throw new UnsupportedOperationException();
}
@Override
public Node getParentNode()
{
throw new UnsupportedOperationException();
}
@Override
public Node getPreviousSibling()
{
throw new UnsupportedOperationException();
}
@Override
public String getTextContent() throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public Object getUserData(String key)
{
throw new UnsupportedOperationException();
}
@Override
public boolean hasChildNodes()
{
throw new UnsupportedOperationException();
}
@Override
public Node insertBefore(Node newChild, Node refChild) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public boolean isDefaultNamespace(String namespaceURI)
{
throw new UnsupportedOperationException();
}
@Override
public boolean isEqualNode(Node arg)
{
throw new UnsupportedOperationException();
}
@Override
public boolean isSameNode(Node other)
{
throw new UnsupportedOperationException();
}
@Override
public boolean isSupported(String feature, String version)
{
throw new UnsupportedOperationException();
}
@Override
public String lookupPrefix(String namespaceURI)
{
throw new UnsupportedOperationException();
}
@Override
public void normalize()
{
throw new UnsupportedOperationException();
}
@Override
public Node removeChild(Node oldChild) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public Node replaceChild(Node newChild, Node oldChild) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public void setNodeValue(String nodeValue) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public void setPrefix(String prefix) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public void setTextContent(String textContent) throws DOMException
{
throw new UnsupportedOperationException();
}
@Override
public Object setUserData(String key, Object data, UserDataHandler handler)
{
throw new UnsupportedOperationException();
}
@Override
public NamedNodeMap getAttributes()
{
throw new UnsupportedOperationException();
}
@Override
public String getLocalName()
{
throw new UnsupportedOperationException();
}
@Override
public String getNamespaceURI()
{
throw new UnsupportedOperationException();
}
@Override
public short getNodeType()
{
throw new UnsupportedOperationException();
}
@Override
public String getPrefix()
{
throw new UnsupportedOperationException();
}
@Override
public boolean hasAttributes()
{
throw new UnsupportedOperationException();
}
@Override
public String lookupNamespaceURI(String prefix)
{
throw new UnsupportedOperationException();
}
}