/*
* Copyright (C) 2014 Alec Dhuse
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 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 General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package co.foldingmap.xml;
import co.foldingmap.dataStructures.SmartTokenizer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.StringTokenizer;
/**
* Represents an XML tag.
*
* @author Alec
*/
public class XMLTag {
ArrayList<XMLTag> subTags;
HashMap<String, String> properties;
String tagName, tagContent;
/**
* Constructor for objects of class XMLTag
*
* @param tagName
* @param subTag
*/
public XMLTag(String tagName, XMLTag subTag) {
this.tagName = (tagName);
this.subTags = new ArrayList<XMLTag>();
subTags.add(subTag);
}
/**
* Constructor for objects of class XMLTag
*
* @param tagName
* @param tagContent
*
*/
public XMLTag(String tagName, String tagContent) {
this.tagName = (tagName);
this.tagContent = (tagContent);
subTags = new ArrayList<XMLTag>();
//Tag has Properties
if (tagName.contains("=")) {
parseProperties(tagName);
}
if (tagName.equalsIgnoreCase("description")) {
/**
* Do nothing, because it could contain other tags, but we want
* to leave it in the description tag and not break it up.
*/
} else if (tagContent.contains("<")) {
//there are subtags
parseSubTags();
}
}
/**
* Adds a given property to this XMLTag.
*
* @param name
* @param value
*/
public void addProperty(String name, String value) {
if (properties == null) properties = new HashMap<String, String>();
properties.put(name, value);
}
/**
* Adds a subtag to this XML Tag.
*
* @param newSubtag
*/
public void addSubtag(XMLTag newSubtag) {
subTags.add(newSubtag);
}
/**
* Adds a list of XML Tags to this XML Tag.
*
* @param newSubtags
*/
public void addSubtags(ArrayList<XMLTag> newSubtags) {
subTags.addAll(newSubtags);
}
/**
* Returns if this tag has a subtag with a given name.
*
* @param subtagName
* @return
*/
public boolean containsSubTag(String subtagName) {
boolean subtagExists = false;
String token;
StringTokenizer st;
for (XMLTag currentTag: subTags) {
if (!currentTag.getTagName().contains(" ")) {
if (currentTag.getTagName().equalsIgnoreCase(subtagName)) {
subtagExists = true;
break;
}
} else {
st = new StringTokenizer(currentTag.getTagName());
token = st.nextToken();
if (token.equalsIgnoreCase(subtagName)) {
subtagExists = true;
break;
}
}
}
return subtagExists;
}
/**
* Converts a String with HTML safe text to normal text.
*
* @param safeText
* @return
*/
public static String convertSafeText(String safeText) {
String text;
text = safeText.replaceAll("'", "'");
text = text.replaceAll("&" , "&");
return text;
}
/**
* Creates an XML tag object and calls the toString method,
* returning the result.
*
* @param tagName
* @param tagContent
* @return
*/
public static String createTagText(String tagName, String tagContent) {
XMLTag newTag;
newTag = new XMLTag(tagName, tagContent);
return newTag.toString();
}
/**
* Returns a list of the gx subtags contained in this XML tag.
*
* @return
*/
public ArrayList<XMLTag> getGxSubtags() {
ArrayList<XMLTag> gxSubtags = new ArrayList<XMLTag>();
for (XMLTag currentTag: subTags) {
if (currentTag.getTagName().startsWith("gx:")) {
gxSubtags.add(currentTag);
}
}
return gxSubtags;
}
/**
* Returns the value of the given tag property.
* If the tag does not exist a blank String is returned.
*
* @param property
* @return
*/
public String getPropertyValue(String property) {
if (properties == null) {
return "";
} else {
String value = properties.get(property);
if (value == null) {
return "";
} else {
return value;
}
}
}
/**
* Returns XML safe text, for writing to a file.
*
* @param text
* @return
*/
public static String getSafeText(String text) {
if (text != null && text.length() > 0) {
text = text.replaceAll("'", "'");
text = text.replaceAll("&", "&");
}
return text;
}
/**
* Returns the content of this tag.
*
* @return
*/
public String getTagContent() {
return convertSafeText(tagContent);
}
/**
* Returns the name of this tag.
*
* @return
*/
public String getTagName() {
return tagName;
}
/**
* Returns the content of a subtag with a given name.
*
* @param name
* @return
*/
public String getSubtagContent(String name) {
try {
String tagContent = new String();
XMLTag currentTag;
for (int i = 0; i < subTags.size(); i++) {
currentTag = (XMLTag) subTags.get(i);
if (currentTag.getTagName().equalsIgnoreCase(name)) {
tagContent = currentTag.getTagContent();
break;
}
}
return tagContent;
} catch (Exception e) {
System.err.println("Error Getting Subtag Content(String) - " + e);
return "";
}
}
/**
* Returns an ArrayList of all subtags in this tag.
*
* @return
*/
public ArrayList<XMLTag> getSubtags() {
return subTags;
}
/**
* Returns the subtag as an XML String.
*
* @return
*/
public String getSubtagsAsString() {
StringBuilder string = new StringBuilder();
for (XMLTag currentSubTag: subTags) {
string.append(currentSubTag.toString());
}
return string.toString();
}
/**
* Returns the first occurance of a XMLTag with the supplied name
* @param name
* @return
*/
public XMLTag getSubtag(String name) {
String token;
StringTokenizer st;
XMLTag returnTag = null;
for (XMLTag currentSubTag: subTags) {
if (!currentSubTag.getTagName().contains(" ")) {
if (currentSubTag.getTagName().equalsIgnoreCase(name)) {
returnTag = currentSubTag;
break;
}
} else {
st = new StringTokenizer(currentSubTag.getTagName());
token = st.nextToken();
if (token.equalsIgnoreCase(name)) {
returnTag = currentSubTag;
break;
}
}
}
return returnTag;
}
/**
* Returns all subtags that match the name supplied.
*
* @param name
* @return
* @deprecated
*/
public ArrayList<XMLTag> getSubtags(String name) {
ArrayList<XMLTag> returnSubtags = new ArrayList<XMLTag>();
XMLTag currentTag;
for (int i = 0; i < subTags.size(); i++) {
currentTag = (XMLTag) subTags.get(i);
if (currentTag.getTagName().equalsIgnoreCase(name))
returnSubtags.add(currentTag);
}
return returnSubtags;
}
/**
* Returns all subtags that match the name supplied.
* This version works better on tags with properties.
*
* @param name
* @return
*/
public ArrayList<XMLTag> getSubtagsByName(String name) {
ArrayList<XMLTag> returnSubtags = new ArrayList<XMLTag>();
int tagNameStop;
String tagName;
XMLTag currentTag;
for (int i = 0; i < subTags.size(); i++) {
currentTag = (XMLTag) subTags.get(i);
tagNameStop = currentTag.getTagName().indexOf(" ");
if (tagNameStop < 0) tagNameStop = currentTag.getTagName().length();
tagName = currentTag.getTagName().substring(0, tagNameStop);
if (tagName.equalsIgnoreCase(name))
returnSubtags.add(currentTag);
}
return returnSubtags;
}
/**
* Returns the first subtag that starts with the given text.
*
* @param prefix
* @return
*/
public XMLTag getSubtagStartsWith(String prefix) {
XMLTag returnTag = null;
for (XMLTag currentSubTag: subTags) {
if (currentSubTag.getTagName().startsWith(prefix)) {
returnTag = currentSubTag;
break;
}
}
return returnTag;
}
/**
* Returns subtags that match the name supplied.
*
* @param name
* @return
*/
public ArrayList<XMLTag> getTagSubtags(String name) {
ArrayList<XMLTag> returnSubtags = new ArrayList<XMLTag>();
XMLTag currentTag;
for (int i = 0; i < subTags.size(); i++) {
currentTag = (XMLTag) subTags.get(i);
if (currentTag.getTagName().equalsIgnoreCase(name)) {
returnSubtags = currentTag.getSubtags();
break;
}
}
return returnSubtags;
}
/**
* Returns all subtags with a given name.
*
* @param name
* @return
*/
public ArrayList<XMLTag> getTags(String name) {
String nameToSearchFor = name;
StringTokenizer st;
ArrayList<XMLTag> tags = new ArrayList<XMLTag>();
XMLTag currentTag;
try {
for (int i = 0; i < subTags.size(); i++) {
currentTag = (XMLTag) subTags.get(i);
st = new StringTokenizer(currentTag.getTagName());
if (st.hasMoreTokens())
nameToSearchFor = st.nextToken();
if (nameToSearchFor.equalsIgnoreCase(name)) {
tags.add(currentTag);
}
}
} catch (Exception e) {
System.err.println("Error in XMLTag.getTags(" + name + ") - " + e);
}
return tags;
}
/**
* For tags that have the form <object id="something">
* Returns the value of a tag, the value being the text between the
* quotation marks of a tag's name.
*
* If no value is present, an empty string is returned.
*
* @return
* @deprecated
*/
public String getTagValue() {
int start, end;
String value;
start = tagName.indexOf("\"");
if (start > 0) {
end = tagName.lastIndexOf("\"");
value = tagName.substring(start + 1, end);
return value;
} else {
return "";
}
}
/**
* Returns if this tag has subtags.
*
* @return
*/
public boolean hasSubTags() {
if (subTags.size() > 0) {
return true;
} else {
return false;
}
}
/**
* Returns true if the text matches the start of this tags name.
* Matches are case insensitive.
*
* @param text
* @return
*/
public boolean nameStartsWith(String text) {
String start;
start = this.tagName.substring(0, text.length());
return (text.equalsIgnoreCase(start));
}
/**
* Parses out tag properties.
*
* @param tagName
*/
private void parseProperties(String tagName) {
//Tag has Properties
if (properties == null)
properties = new HashMap<String, String>();
SmartTokenizer st = new SmartTokenizer(tagName);
while (st.hasMore()) {
st.jumpAfterChar(' ');
String propName = st.getTextTo('=');
st.jumpAfterChar('"');
String propValue = st.getTextTo('"');
properties.put(propName, propValue);
}
}
/**
* Parses subtag text ad creates XMLTag objects from it.
*
*/
private void parseSubTags() {
int equalsIndex, tagStart, tagEnd, contentStart, offset;
String currentToken, tagName, subTagContent, subTagName, closingTag;
StringTokenizer st, stEquals;
XMLTag newTag, newSubtag;
offset = 0;
closingTag = "";
try {
while (offset < tagContent.length()) {
tagStart = tagContent.indexOf("<", offset);
tagEnd = tagContent.indexOf(">", offset);
contentStart = (tagEnd + 1);
if ((tagStart >= 0) && (tagEnd >= 0) && (tagEnd > tagStart)) {
tagName = tagContent.substring(tagStart + 1, tagEnd);
st = new StringTokenizer(tagName);
//if ((tagName.endsWith("/")) || (tagName.indexOf("=") >= 0)) {
if ((tagName.endsWith("/"))) {
//for single tags like <br />
newTag = new XMLTag(st.nextToken(), "");
if (tagName.indexOf("=") >= 0) {
//tags with values in them like <node id="619207332"
stEquals = new StringTokenizer(tagName);
while (stEquals.hasMoreElements()) {
currentToken = stEquals.nextToken();
equalsIndex = currentToken.indexOf("=");
if (equalsIndex >= 0) {
subTagName = currentToken.substring(0, equalsIndex);
subTagContent = currentToken.substring(equalsIndex + 1);
if (subTagContent.startsWith("\"")) {
subTagContent = subTagContent.substring(1);
}
if (subTagContent.endsWith("\"")) {
subTagContent = subTagContent.substring(0, subTagContent.length() - 1);
}
if (subTagContent.endsWith("\"/")) {
subTagContent = subTagContent.substring(0, subTagContent.length() - 2);
}
newSubtag = new XMLTag(subTagName, subTagContent);
newTag.addSubtag(newSubtag);
}
} //end while loop
offset = tagEnd + 1;
} else {
offset = tagContent.length();
tagContent = "";
}
addSubtag(newTag);
//tagContent = "";
} else {
if (tagName.endsWith("/")) {
//for closing tags
tagStart = tagName.indexOf(" ");
tagEnd = tagName.indexOf("/");
subTagContent = tagName.substring(tagStart, tagEnd);
subTagContent = subTagContent.trim();
offset = contentStart + 1;
tagName = st.nextToken();
newTag = new XMLTag(tagName, subTagContent);
subTags.add(newTag);
} else {
if (st.countTokens() == 1) {
closingTag = "</" + tagName + ">";
tagStart = tagContent.indexOf(closingTag, offset);
} else if (st.countTokens() > 1) {
//tags of the format: <Polygon id="University">
closingTag = "</" + st.nextToken() + ">";
tagStart = tagContent.indexOf(closingTag, offset);
}
if (tagStart >= 0) {
subTagContent = tagContent.substring((tagEnd + 1) , tagStart);
subTagContent = subTagContent.trim();
newTag = new XMLTag(tagName, subTagContent);
offset = (tagStart + closingTag.length());
subTags.add(newTag);
} else {
offset = contentStart;
}
}
}
} else {
offset = tagContent.length();
}
}
if (subTags.size() > 0) {
tagContent = "";
} else {
//no subtags, preserve memory
subTags.trimToSize();
}
} catch (Exception e) {
System.err.println("Error Parsing Subtags of " + this.tagName + ": " + e);
}
} // end of parseSubTags
/**
* Outputs a String in XML of this tag.
*
* @return
*/
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("<");
string.append(tagName);
string.append(">");
string.append(getSafeText(tagContent));
for (XMLTag currentSubTag: subTags) {
string.append(currentSubTag.toString());
}
string.append("</");
string.append(tagName);
string.append(">");
return string.toString();
}
}