/* ============================================================================== This file is part of the JUCE library - "Jules' Utility Class Extensions" Copyright 2004-11 by Raw Material Software Ltd. ------------------------------------------------------------------------------ JUCE can be redistributed and/or modified under the terms of the GNU General Public License (Version 2), as published by the Free Software Foundation. A copy of the license is included in the JUCE distribution, or can be found online at www.gnu.org/licenses. JUCE 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. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.rawmaterialsoftware.com/juce for more information. ============================================================================== */ XmlElement::XmlAttributeNode::XmlAttributeNode (const XmlAttributeNode& other) noexcept : name (other.name), value (other.value) { } XmlElement::XmlAttributeNode::XmlAttributeNode (const String& name_, const String& value_) noexcept : name (name_), value (value_) { #if JUCE_DEBUG // this checks whether the attribute name string contains any illegal characters.. for (String::CharPointerType t (name.getCharPointer()); ! t.isEmpty(); ++t) jassert (t.isLetterOrDigit() || *t == '_' || *t == '-' || *t == ':'); #endif } inline bool XmlElement::XmlAttributeNode::hasName (const String& nameToMatch) const noexcept { return name.equalsIgnoreCase (nameToMatch); } //============================================================================== XmlElement::XmlElement (const String& tagName_) noexcept : tagName (tagName_) { // the tag name mustn't be empty, or it'll look like a text element! jassert (tagName_.containsNonWhitespaceChars()) // The tag can't contain spaces or other characters that would create invalid XML! jassert (! tagName_.containsAnyOf (" <>/&")); } XmlElement::XmlElement (int /*dummy*/) noexcept { } XmlElement::XmlElement (const XmlElement& other) : tagName (other.tagName) { copyChildrenAndAttributesFrom (other); } XmlElement& XmlElement::operator= (const XmlElement& other) { if (this != &other) { removeAllAttributes(); deleteAllChildElements(); tagName = other.tagName; copyChildrenAndAttributesFrom (other); } return *this; } #if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS XmlElement::XmlElement (XmlElement&& other) noexcept : nextListItem (static_cast &&> (other.nextListItem)), firstChildElement (static_cast &&> (other.firstChildElement)), attributes (static_cast &&> (other.attributes)), tagName (static_cast (other.tagName)) { } XmlElement& XmlElement::operator= (XmlElement&& other) noexcept { jassert (this != &other); // hopefully the compiler should make this situation impossible! removeAllAttributes(); deleteAllChildElements(); nextListItem = static_cast &&> (other.nextListItem); firstChildElement = static_cast &&> (other.firstChildElement); attributes = static_cast &&> (other.attributes); tagName = static_cast (other.tagName); return *this; } #endif void XmlElement::copyChildrenAndAttributesFrom (const XmlElement& other) { jassert (firstChildElement.get() == nullptr); firstChildElement.addCopyOfList (other.firstChildElement); jassert (attributes.get() == nullptr); attributes.addCopyOfList (other.attributes); } XmlElement::~XmlElement() noexcept { firstChildElement.deleteAll(); attributes.deleteAll(); } //============================================================================== namespace XmlOutputFunctions { #if 0 // (These functions are just used to generate the lookup table used below) bool isLegalXmlCharSlow (const juce_wchar character) noexcept { if ((character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9')) return true; const char* t = " .,;:-()_+=?!'#@[]/\\*%~{}$|"; do { if (((juce_wchar) (uint8) *t) == character) return true; } while (*++t != 0); return false; } void generateLegalCharLookupTable() { uint8 n[32] = { 0 }; for (int i = 0; i < 256; ++i) if (isLegalXmlCharSlow (i)) n[i >> 3] |= (1 << (i & 7)); String s; for (int i = 0; i < 32; ++i) s << (int) n[i] << ", "; DBG (s); } #endif static bool isLegalXmlChar (const uint32 c) noexcept { static const unsigned char legalChars[] = { 0, 0, 0, 0, 187, 255, 255, 175, 255, 255, 255, 191, 254, 255, 255, 127 }; return c < sizeof (legalChars) * 8 && (legalChars [c >> 3] & (1 << (c & 7))) != 0; } static void escapeIllegalXmlChars (OutputStream& outputStream, const String& text, const bool changeNewLines) { String::CharPointerType t (text.getCharPointer()); for (;;) { const uint32 character = (uint32) t.getAndAdvance(); if (character == 0) break; if (isLegalXmlChar (character)) { outputStream << (char) character; } else { switch (character) { case '&': outputStream << "&"; break; case '"': outputStream << """; break; case '>': outputStream << ">"; break; case '<': outputStream << "<"; break; case '\n': case '\r': if (! changeNewLines) { outputStream << (char) character; break; } // Note: deliberate fall-through here! default: outputStream << "&#" << ((int) character) << ';'; break; } } } } static void writeSpaces (OutputStream& out, const int numSpaces) { out.writeRepeatedByte (' ', numSpaces); } } void XmlElement::writeElementAsText (OutputStream& outputStream, const int indentationLevel, const int lineWrapLength) const { using namespace XmlOutputFunctions; if (indentationLevel >= 0) writeSpaces (outputStream, indentationLevel); if (! isTextElement()) { outputStream.writeByte ('<'); outputStream << tagName; { const int attIndent = indentationLevel + tagName.length() + 1; int lineLen = 0; for (const XmlAttributeNode* att = attributes; att != nullptr; att = att->nextListItem) { if (lineLen > lineWrapLength && indentationLevel >= 0) { outputStream << newLine; writeSpaces (outputStream, attIndent); lineLen = 0; } const int64 startPos = outputStream.getPosition(); outputStream.writeByte (' '); outputStream << att->name; outputStream.write ("=\"", 2); escapeIllegalXmlChars (outputStream, att->value, true); outputStream.writeByte ('"'); lineLen += (int) (outputStream.getPosition() - startPos); } } if (firstChildElement != nullptr) { outputStream.writeByte ('>'); bool lastWasTextNode = false; for (XmlElement* child = firstChildElement; child != nullptr; child = child->nextListItem) { if (child->isTextElement()) { escapeIllegalXmlChars (outputStream, child->getText(), false); lastWasTextNode = true; } else { if (indentationLevel >= 0 && ! lastWasTextNode) outputStream << newLine; child->writeElementAsText (outputStream, lastWasTextNode ? 0 : (indentationLevel + (indentationLevel >= 0 ? 2 : 0)), lineWrapLength); lastWasTextNode = false; } } if (indentationLevel >= 0 && ! lastWasTextNode) { outputStream << newLine; writeSpaces (outputStream, indentationLevel); } outputStream.write ("'); } else { outputStream.write ("/>", 2); } } else { escapeIllegalXmlChars (outputStream, getText(), false); } } String XmlElement::createDocument (const String& dtdToUse, const bool allOnOneLine, const bool includeXmlHeader, const String& encodingType, const int lineWrapLength) const { MemoryOutputStream mem (2048); writeToStream (mem, dtdToUse, allOnOneLine, includeXmlHeader, encodingType, lineWrapLength); return mem.toUTF8(); } void XmlElement::writeToStream (OutputStream& output, const String& dtdToUse, const bool allOnOneLine, const bool includeXmlHeader, const String& encodingType, const int lineWrapLength) const { using namespace XmlOutputFunctions; if (includeXmlHeader) { output << ""; if (allOnOneLine) output.writeByte (' '); else output << newLine << newLine; } if (dtdToUse.isNotEmpty()) { output << dtdToUse; if (allOnOneLine) output.writeByte (' '); else output << newLine; } writeElementAsText (output, allOnOneLine ? -1 : 0, lineWrapLength); if (! allOnOneLine) output << newLine; } bool XmlElement::writeToFile (const File& file, const String& dtdToUse, const String& encodingType, const int lineWrapLength) const { if (file.hasWriteAccess()) { TemporaryFile tempFile (file); ScopedPointer out (tempFile.getFile().createOutputStream()); if (out != nullptr) { writeToStream (*out, dtdToUse, false, true, encodingType, lineWrapLength); out = nullptr; return tempFile.overwriteTargetFileWithTemporary(); } } return false; } //============================================================================== bool XmlElement::hasTagName (const String& tagNameWanted) const noexcept { #if JUCE_DEBUG // if debugging, check that the case is actually the same, because // valid xml is case-sensitive, and although this lets it pass, it's // better not to.. if (tagName.equalsIgnoreCase (tagNameWanted)) { jassert (tagName == tagNameWanted); return true; } else { return false; } #else return tagName.equalsIgnoreCase (tagNameWanted); #endif } XmlElement* XmlElement::getNextElementWithTagName (const String& requiredTagName) const { XmlElement* e = nextListItem; while (e != nullptr && ! e->hasTagName (requiredTagName)) e = e->nextListItem; return e; } //============================================================================== int XmlElement::getNumAttributes() const noexcept { return attributes.size(); } const String& XmlElement::getAttributeName (const int index) const noexcept { const XmlAttributeNode* const att = attributes [index]; return att != nullptr ? att->name : String::empty; } const String& XmlElement::getAttributeValue (const int index) const noexcept { const XmlAttributeNode* const att = attributes [index]; return att != nullptr ? att->value : String::empty; } bool XmlElement::hasAttribute (const String& attributeName) const noexcept { for (const XmlAttributeNode* att = attributes; att != nullptr; att = att->nextListItem) if (att->hasName (attributeName)) return true; return false; } //============================================================================== const String& XmlElement::getStringAttribute (const String& attributeName) const noexcept { for (const XmlAttributeNode* att = attributes; att != nullptr; att = att->nextListItem) if (att->hasName (attributeName)) return att->value; return String::empty; } String XmlElement::getStringAttribute (const String& attributeName, const String& defaultReturnValue) const { for (const XmlAttributeNode* att = attributes; att != nullptr; att = att->nextListItem) if (att->hasName (attributeName)) return att->value; return defaultReturnValue; } int XmlElement::getIntAttribute (const String& attributeName, const int defaultReturnValue) const { for (const XmlAttributeNode* att = attributes; att != nullptr; att = att->nextListItem) if (att->hasName (attributeName)) return att->value.getIntValue(); return defaultReturnValue; } double XmlElement::getDoubleAttribute (const String& attributeName, const double defaultReturnValue) const { for (const XmlAttributeNode* att = attributes; att != nullptr; att = att->nextListItem) if (att->hasName (attributeName)) return att->value.getDoubleValue(); return defaultReturnValue; } bool XmlElement::getBoolAttribute (const String& attributeName, const bool defaultReturnValue) const { for (const XmlAttributeNode* att = attributes; att != nullptr; att = att->nextListItem) { if (att->hasName (attributeName)) { juce_wchar firstChar = att->value[0]; if (CharacterFunctions::isWhitespace (firstChar)) firstChar = att->value.trimStart() [0]; return firstChar == '1' || firstChar == 't' || firstChar == 'y' || firstChar == 'T' || firstChar == 'Y'; } } return defaultReturnValue; } bool XmlElement::compareAttribute (const String& attributeName, const String& stringToCompareAgainst, const bool ignoreCase) const noexcept { for (const XmlAttributeNode* att = attributes; att != nullptr; att = att->nextListItem) if (att->hasName (attributeName)) return ignoreCase ? att->value.equalsIgnoreCase (stringToCompareAgainst) : att->value == stringToCompareAgainst; return false; } //============================================================================== void XmlElement::setAttribute (const String& attributeName, const String& value) { if (attributes == nullptr) { attributes = new XmlAttributeNode (attributeName, value); } else { XmlAttributeNode* att = attributes; for (;;) { if (att->hasName (attributeName)) { att->value = value; break; } else if (att->nextListItem == nullptr) { att->nextListItem = new XmlAttributeNode (attributeName, value); break; } att = att->nextListItem; } } } void XmlElement::setAttribute (const String& attributeName, const int number) { setAttribute (attributeName, String (number)); } void XmlElement::setAttribute (const String& attributeName, const double number) { setAttribute (attributeName, String (number)); } void XmlElement::removeAttribute (const String& attributeName) noexcept { LinkedListPointer* att = &attributes; while (att->get() != nullptr) { if (att->get()->hasName (attributeName)) { delete att->removeNext(); break; } att = &(att->get()->nextListItem); } } void XmlElement::removeAllAttributes() noexcept { attributes.deleteAll(); } //============================================================================== int XmlElement::getNumChildElements() const noexcept { return firstChildElement.size(); } XmlElement* XmlElement::getChildElement (const int index) const noexcept { return firstChildElement [index].get(); } XmlElement* XmlElement::getChildByName (const String& childName) const noexcept { for (XmlElement* child = firstChildElement; child != nullptr; child = child->nextListItem) if (child->hasTagName (childName)) return child; return nullptr; } void XmlElement::addChildElement (XmlElement* const newNode) noexcept { if (newNode != nullptr) firstChildElement.append (newNode); } void XmlElement::insertChildElement (XmlElement* const newNode, int indexToInsertAt) noexcept { if (newNode != nullptr) { removeChildElement (newNode, false); firstChildElement.insertAtIndex (indexToInsertAt, newNode); } } XmlElement* XmlElement::createNewChildElement (const String& childTagName) { XmlElement* const newElement = new XmlElement (childTagName); addChildElement (newElement); return newElement; } bool XmlElement::replaceChildElement (XmlElement* const currentChildElement, XmlElement* const newNode) noexcept { if (newNode != nullptr) { LinkedListPointer* const p = firstChildElement.findPointerTo (currentChildElement); if (p != nullptr) { if (currentChildElement != newNode) delete p->replaceNext (newNode); return true; } } return false; } void XmlElement::removeChildElement (XmlElement* const childToRemove, const bool shouldDeleteTheChild) noexcept { if (childToRemove != nullptr) { firstChildElement.remove (childToRemove); if (shouldDeleteTheChild) delete childToRemove; } } bool XmlElement::isEquivalentTo (const XmlElement* const other, const bool ignoreOrderOfAttributes) const noexcept { if (this != other) { if (other == nullptr || tagName != other->tagName) return false; if (ignoreOrderOfAttributes) { int totalAtts = 0; for (const XmlAttributeNode* att = attributes; att != nullptr; att = att->nextListItem) { if (! other->compareAttribute (att->name, att->value)) return false; ++totalAtts; } if (totalAtts != other->getNumAttributes()) return false; } else { const XmlAttributeNode* thisAtt = attributes; const XmlAttributeNode* otherAtt = other->attributes; for (;;) { if (thisAtt == nullptr || otherAtt == nullptr) { if (thisAtt == otherAtt) // both 0, so it's a match break; return false; } if (thisAtt->name != otherAtt->name || thisAtt->value != otherAtt->value) { return false; } thisAtt = thisAtt->nextListItem; otherAtt = otherAtt->nextListItem; } } const XmlElement* thisChild = firstChildElement; const XmlElement* otherChild = other->firstChildElement; for (;;) { if (thisChild == nullptr || otherChild == nullptr) { if (thisChild == otherChild) // both 0, so it's a match break; return false; } if (! thisChild->isEquivalentTo (otherChild, ignoreOrderOfAttributes)) return false; thisChild = thisChild->nextListItem; otherChild = otherChild->nextListItem; } } return true; } void XmlElement::deleteAllChildElements() noexcept { firstChildElement.deleteAll(); } void XmlElement::deleteAllChildElementsWithTagName (const String& name) noexcept { XmlElement* child = firstChildElement; while (child != nullptr) { XmlElement* const nextChild = child->nextListItem; if (child->hasTagName (name)) removeChildElement (child, true); child = nextChild; } } bool XmlElement::containsChildElement (const XmlElement* const possibleChild) const noexcept { return firstChildElement.contains (possibleChild); } XmlElement* XmlElement::findParentElementOf (const XmlElement* const elementToLookFor) noexcept { if (this == elementToLookFor || elementToLookFor == nullptr) return nullptr; for (XmlElement* child = firstChildElement; child != nullptr; child = child->nextListItem) { if (elementToLookFor == child) return this; XmlElement* const found = child->findParentElementOf (elementToLookFor); if (found != nullptr) return found; } return nullptr; } void XmlElement::getChildElementsAsArray (XmlElement** elems) const noexcept { firstChildElement.copyToArray (elems); } void XmlElement::reorderChildElements (XmlElement** const elems, const int num) noexcept { XmlElement* e = firstChildElement = elems[0]; for (int i = 1; i < num; ++i) { e->nextListItem = elems[i]; e = e->nextListItem; } e->nextListItem = nullptr; } //============================================================================== bool XmlElement::isTextElement() const noexcept { return tagName.isEmpty(); } static const String juce_xmltextContentAttributeName ("text"); const String& XmlElement::getText() const noexcept { jassert (isTextElement()); // you're trying to get the text from an element that // isn't actually a text element.. If this contains text sub-nodes, you // probably want to use getAllSubText instead. return getStringAttribute (juce_xmltextContentAttributeName); } void XmlElement::setText (const String& newText) { if (isTextElement()) setAttribute (juce_xmltextContentAttributeName, newText); else jassertfalse; // you can only change the text in a text element, not a normal one. } String XmlElement::getAllSubText() const { if (isTextElement()) return getText(); MemoryOutputStream mem (1024); for (const XmlElement* child = firstChildElement; child != nullptr; child = child->nextListItem) mem << child->getAllSubText(); return mem.toString(); } String XmlElement::getChildElementAllSubText (const String& childTagName, const String& defaultReturnValue) const { const XmlElement* const child = getChildByName (childTagName); if (child != nullptr) return child->getAllSubText(); return defaultReturnValue; } XmlElement* XmlElement::createTextElement (const String& text) { XmlElement* const e = new XmlElement ((int) 0); e->setAttribute (juce_xmltextContentAttributeName, text); return e; } void XmlElement::addTextElement (const String& text) { addChildElement (createTextElement (text)); } void XmlElement::deleteAllTextElements() noexcept { XmlElement* child = firstChildElement; while (child != nullptr) { XmlElement* const next = child->nextListItem; if (child->isTextElement()) removeChildElement (child, true); child = next; } }