/*****************************************************************************
* This file is part of Rinzo
*
* Author: Claudio Cancinos
* WWW: https://sourceforge.net/projects/editorxml
* Copyright (C): 2008, Claudio Cancinos
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; If not, see <http://www.gnu.org/licenses/>
****************************************************************************/
package ar.com.tadp.xml.rinzo.core.model;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IDocumentPartitioningListener;
import org.eclipse.jface.text.TypedPosition;
import ar.com.tadp.xml.rinzo.XMLEditorPlugin;
import ar.com.tadp.xml.rinzo.core.RinzoXMLEditor;
import ar.com.tadp.xml.rinzo.core.model.tags.TagTypeDefinition;
import ar.com.tadp.xml.rinzo.core.model.visitor.HierarchicalVisitor;
import ar.com.tadp.xml.rinzo.core.model.visitor.Visitable;
import ar.com.tadp.xml.rinzo.core.partitioner.IXMLPartitions;
/**
* An object representing a node in the xml file. Similar to a DOM object
*
* @author ccancinos
*/
public class XMLNode extends TypedPosition implements Visitable, IDocumentListener, IDocumentPartitioningListener {
private XMLNode parent;
private List<XMLNode> children;
/** Reference to the opening/end tag as needed */
private XMLNode correspondingNode;
private String fullTagName = "";
private String tagName = "";
private String namespace = "";
private Map<String, XMLAttribute> attributes = new HashMap<String, XMLAttribute>();
protected boolean documentChanged = true;
private IDocument document;
private RinzoXMLEditor editor;
public XMLNode(int offset, int length, String type, IDocument document) {
super(offset, length, type);
children = new ArrayList<XMLNode>();
this.document = document;
if (document != null) {
this.addListeners();
}
}
public void documentAboutToBeChanged(DocumentEvent event) {
}
public void hasChanged() {
this.documentChanged = true;
}
public void documentChanged(DocumentEvent event) {
documentChanged = true;
}
public void documentPartitioningChanged(IDocument document) {
documentChanged = true;
}
private void addListeners() {
this.document.addDocumentListener(this);
this.document.addDocumentPartitioningListener(this);
}
public IDocument getDocument() {
return this.document;
}
public TagTypeDefinition getTypeDefinition() {
return this.editor.getTagContainersRegistry().getTagDefinition(this);
}
public void setEditor(RinzoXMLEditor editor) {
this.editor = editor;
}
public RinzoXMLEditor getEditor() {
return this.editor;
}
public void setParent(XMLNode node) {
this.parent = node;
}
public XMLNode getParent() {
return (this.isLastNode() && this.isTag()) ? this : this.parent;
}
public List<XMLNode> getChildren() {
return this.children;
}
// Used by the outline
public XMLNode[] getChildren(boolean includeEmptyText) {
if (includeEmptyText) {
return this.children.toArray(new XMLNode[this.children.size()]);
}
List<XMLNode> excludeEmptyText = new ArrayList<XMLNode>();
for (Iterator<XMLNode> iter = this.children.iterator(); iter.hasNext();) {
XMLNode node = iter.next();
if (!node.isEmpty()) {
excludeEmptyText.add(node);
}
}
return excludeEmptyText.toArray(new XMLNode[excludeEmptyText.size()]);
}
public void addChild(XMLNode node) {
node.setParent(this);
this.children.add(node);
}
public String getContent() {
try {
return document.get(offset, length);
} catch (BadLocationException e) {
e.printStackTrace();
}
return "";
}
public String getFullTagName() {
if (this.documentChanged || StringUtils.isEmpty(this.fullTagName)) {
this.documentChanged = false;
StringTokenizer tagNameTokenizer = new StringTokenizer(this.getContent(), " \t\n\r<>/");
this.fullTagName = (tagNameTokenizer.hasMoreTokens()) ? tagNameTokenizer.nextToken() : "";
}
return this.fullTagName;
}
public String getTagName() {
if (this.documentChanged || StringUtils.isEmpty(this.tagName)) {
this.tagName = (this.getFullTagName().contains(":")) ?
this.getFullTagName().substring(this.getFullTagName().indexOf(":") + 1) : this.getFullTagName();
}
return this.tagName;
}
public String getNamespace() {
if (this.documentChanged || StringUtils.isEmpty(this.namespace)) {
this.namespace = (this.getFullTagName().contains(":")) ?
this.getFullTagName().substring(0, this.getFullTagName().indexOf(":")) :
"";
}
return this.namespace;
}
public String getXPath() {
if (this.isEndTag()) {
return this.getCorrespondingNode().getXPath();
}
if (!this.isTag() && !this.isEmptyTag()) {
return this.getParent().getXPath();
}
String xpathName;
if (this.getParent() == null || StringUtils.isEmpty(this.getParent().getContent())) {
xpathName = "/"
+ (StringUtils.isEmpty(this.getNamespace()) ? this.getTagName() : this.getNamespace() + ":"
+ this.getTagName());
} else {
xpathName = this.getParent().getXPath()
+ "/"
+ (StringUtils.isEmpty(this.getNamespace()) ? this.getTagName() : this.getNamespace() + ":"
+ this.getTagName());
int index = 1;
int count = 1;
for (Iterator<XMLNode> iterator = this.getParent().getChildren().iterator(); iterator.hasNext();) {
XMLNode sibling = iterator.next();
if (sibling.getNamespace().equals(this.getNamespace())
&& sibling.getTagName().equals(this.getTagName())) {
if (sibling == this) {
index = count;
} else {
count++;
}
}
}
if (count > 1) {
xpathName += "[" + index + "]";
}
}
return xpathName;
}
/**
* Returns the attributs written in this tag in the editor
*/
public Map<String, XMLAttribute> getAttributes() {
if (this.documentChanged || this.attributes.isEmpty()) {
this.documentChanged = false;
String str = this.getContent();
this.attributes.clear();
char[] charArray = str.toCharArray();
for (int pos = charArray.length - 1; pos >= 0; pos--) {
char currentChar = charArray[pos];
XMLAttribute attribute = null;
if (currentChar == '\"') {
int equalIndex = pos;
pos--;
while (pos > 0 && charArray[pos] != '\"') {
pos--;
}
attribute = new XMLAttribute(str.substring(pos + 1, equalIndex).trim(), this.offset + pos + 1,
equalIndex - pos - 1);
while (pos > 0 && charArray[pos] != '=') {
pos--;
}
currentChar = charArray[pos];
}
if (currentChar == '=') {
int equalIndex = pos;
if (charArray[--pos] == ' ') {
while (charArray[pos] == ' ' && pos > 0)
pos--;
}
while (pos > 0 && charArray[pos] != ' ' && charArray[pos] != '\t' && charArray[pos] != '\r'
&& charArray[pos] != '\n') {
pos--;
}
if (attribute != null) {
attribute.setName(str.substring(pos + 1, equalIndex).trim());
}
}
if (attribute != null) {
this.attributes.put(attribute.getName(), attribute);
}
}
}
return this.attributes;
}
public int getSelectionOffset() {
if (getType().equals(IXMLPartitions.XML_TEXT)) {
return this.getOffset();
}
String tagContent = getContent();
if (tagContent.length() > 0) {
for (int pos = 0; pos <= tagContent.length(); pos++) {
if (tagContent.charAt(pos) != '<' && tagContent.charAt(pos) != '/') {
return this.getOffset() + pos;
}
}
}
return 0;
}
public int getSelectionLength() {
if (getType().equals(IXMLPartitions.XML_TEXT)) {
return this.getLength();
}
return this.getTagName() != null ? this.getTagName().length() : 0;
}
/**
* Devuelve el String sobre el que est� posicionado el cursor
*/
public String getStringAt(int offset) {
int relativeOffset = offset - this.offset;
int start = 0, end = 0;
String content = this.getContent();
StringCharacterIterator iter = new StringCharacterIterator(content);
char c;
for (c = iter.setIndex(relativeOffset); c != CharacterIterator.DONE && this.isFullIdentifierPart(c); c = iter
.previous()) {
}
start = this.isFullIdentifierPart(iter.current()) ? iter.getIndex() : iter.getIndex() + 1;
for (c = iter.setIndex(relativeOffset); c != CharacterIterator.DONE && this.isFullIdentifierPart(c); c = iter
.next()) {
}
end = iter.getIndex();
return (start <= end) ? content.substring(start, end) : "";
}
private boolean isFullIdentifierPart(char c) {
return c != '\"' && c != '\'' && c != '<' && c != '>' && c != ' ' && c != '\n' && c != '\r' && c != '\t' && c != '?';
}
public boolean isEmpty() {
return this.getContent() != null && this.getContent().trim().length() == 0;
}
public boolean isLastNode() {
return this.document.getLength() == offset + length;
}
public boolean hasChildren() {
return this.children != null ? this.children.size() > 0 : false;
}
public void setCorrespondingNode(XMLNode node) {
this.correspondingNode = node;
}
public XMLNode getCorrespondingNode() {
return this.correspondingNode;
}
public String toString() {
return this.getType() + ": " + ((this.length > 0) ? "\"" + this.getContent() + "\"" : "Empty Tag");
}
public boolean accept(HierarchicalVisitor visitor) {
// If I would have separated the concept of composite element and simple
// element (composite and leaf in a tree), this if will be resolved
// polimorphically :(
// if it is a leaf
if (!this.hasChildren() && this.getCorrespondingNode() == null) {
return visitor.visitChild(this);
}
// if it is a composite node
if (visitor.visitStart(this)) {
boolean accept = true;
Iterator<XMLNode> iterator = this.getChildren().iterator();
try {
while (iterator.hasNext() && accept) {
XMLNode child = iterator.next();
accept = child.accept(visitor);
}
} catch (ConcurrentModificationException e) {
XMLEditorPlugin.logErrorMessage("ConcurrentModificationException iterating childs of " + this.getContent(), e);
}
}
return visitor.visitEnd(this);
}
public boolean isRoot() {
XMLNode node = this.getParent();
while (node != null && !node.isTag()) {
node = node.getParent();
}
return (node == null || node instanceof XMLRootNode) ? true : false;
}
public boolean isTag() {
return IXMLPartitions.XML_TAG.equals(this.getType());
}
public boolean isEndTag() {
return IXMLPartitions.XML_ENDTAG.equals(this.getType());
}
public boolean isIncompleteTag() {
return IXMLPartitions.XML_INCOMPLETETAG.equals(this.getType());
}
public boolean isEmptyTag() {
return IXMLPartitions.XML_EMPTYTAG.equals(this.getType());
}
public boolean isTextTag() {
return IXMLPartitions.XML_TEXT.equals(this.getType());
}
public boolean isCommentTag() {
return IXMLPartitions.XML_COMMENT.equals(this.getType());
}
public boolean isDeclarationTag() {
return IXMLPartitions.XML_DECLARATION.equals(this.getType());
}
public boolean isPiTag() {
return IXMLPartitions.XML_PI.equals(this.getType());
}
public boolean isCdata() {
return IXMLPartitions.XML_CDATA.equals(this.getType());
}
public boolean isSchemaRootTag() {
return this.isRoot() && this.getAttributes().containsKey("xmlns");
}
}