1 /*
2 ==============================================================================
3
4 This file is part of the Water library.
5 Copyright (c) 2016 ROLI Ltd.
6 Copyright (C) 2017-2018 Filipe Coelho <falktx@falktx.com>
7
8 Permission is granted to use this software under the terms of the ISC license
9 http://www.isc.org/downloads/software-support-policy/isc-license/
10
11 Permission to use, copy, modify, and/or distribute this software for any
12 purpose with or without fee is hereby granted, provided that the above
13 copyright notice and this permission notice appear in all copies.
14
15 THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD
16 TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
17 FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT,
18 OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
19 USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
20 TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
21 OF THIS SOFTWARE.
22
23 ==============================================================================
24 */
25
26 #include "XmlDocument.h"
27 #include "XmlElement.h"
28 #include "../containers/LinkedListPointer.h"
29 #include "../streams/FileInputSource.h"
30 #include "../streams/InputStream.h"
31 #include "../streams/MemoryOutputStream.h"
32
33 namespace water {
34
XmlDocument(const String & documentText)35 XmlDocument::XmlDocument (const String& documentText)
36 : originalText (documentText),
37 input (nullptr),
38 outOfData (false),
39 errorOccurred (false),
40 needToLoadDTD (false),
41 ignoreEmptyTextElements (true)
42 {
43 }
44
XmlDocument(const File & file)45 XmlDocument::XmlDocument (const File& file)
46 : input (nullptr),
47 outOfData (false),
48 errorOccurred (false),
49 needToLoadDTD (false),
50 ignoreEmptyTextElements (true),
51 inputSource (new FileInputSource (file))
52 {
53 }
54
~XmlDocument()55 XmlDocument::~XmlDocument()
56 {
57 }
58
parse(const File & file)59 XmlElement* XmlDocument::parse (const File& file)
60 {
61 XmlDocument doc (file);
62 return doc.getDocumentElement();
63 }
64
parse(const String & xmlData)65 XmlElement* XmlDocument::parse (const String& xmlData)
66 {
67 XmlDocument doc (xmlData);
68 return doc.getDocumentElement();
69 }
70
setInputSource(FileInputSource * const newSource)71 void XmlDocument::setInputSource (FileInputSource* const newSource) noexcept
72 {
73 inputSource = newSource;
74 }
75
setEmptyTextElementsIgnored(const bool shouldBeIgnored)76 void XmlDocument::setEmptyTextElementsIgnored (const bool shouldBeIgnored) noexcept
77 {
78 ignoreEmptyTextElements = shouldBeIgnored;
79 }
80
81 namespace XmlIdentifierChars
82 {
isIdentifierCharSlow(const water_uchar c)83 static bool isIdentifierCharSlow (const water_uchar c) noexcept
84 {
85 return CharacterFunctions::isLetterOrDigit (c)
86 || c == '_' || c == '-' || c == ':' || c == '.';
87 }
88
isIdentifierChar(const water_uchar c)89 static bool isIdentifierChar (const water_uchar c) noexcept
90 {
91 static const uint32 legalChars[] = { 0, 0x7ff6000, 0x87fffffe, 0x7fffffe, 0 };
92
93 return ((int) c < (int) numElementsInArray (legalChars) * 32) ? ((legalChars [c >> 5] & (1 << (c & 31))) != 0)
94 : isIdentifierCharSlow (c);
95 }
96
97 /*static void generateIdentifierCharConstants()
98 {
99 uint32 n[8] = { 0 };
100 for (int i = 0; i < 256; ++i)
101 if (isIdentifierCharSlow (i))
102 n[i >> 5] |= (1 << (i & 31));
103
104 String s;
105 for (int i = 0; i < 8; ++i)
106 s << "0x" << String::toHexString ((int) n[i]) << ", ";
107
108 DBG (s);
109 }*/
110
findEndOfToken(String::CharPointerType p)111 static String::CharPointerType findEndOfToken (String::CharPointerType p)
112 {
113 while (isIdentifierChar (*p))
114 ++p;
115
116 return p;
117 }
118 }
119
getDocumentElement(const bool onlyReadOuterDocumentElement)120 XmlElement* XmlDocument::getDocumentElement (const bool onlyReadOuterDocumentElement)
121 {
122 if (originalText.isEmpty() && inputSource != nullptr)
123 {
124 CarlaScopedPointer<InputStream> in (inputSource->createInputStream());
125
126 if (in != nullptr)
127 {
128 MemoryOutputStream data;
129 data.writeFromInputStream (*in, onlyReadOuterDocumentElement ? 8192 : -1);
130
131 if (data.getDataSize() > 2)
132 {
133 data.writeByte (0);
134 const char* text = static_cast<const char*> (data.getData());
135
136 if (CharPointer_UTF8::isByteOrderMark (text))
137 text += 3;
138
139 // parse the input buffer directly to avoid copying it all to a string..
140 return parseDocumentElement (String::CharPointerType (text), onlyReadOuterDocumentElement);
141 }
142 }
143 }
144
145 return parseDocumentElement (originalText.getCharPointer(), onlyReadOuterDocumentElement);
146 }
147
getLastParseError() const148 const String& XmlDocument::getLastParseError() const noexcept
149 {
150 return lastError;
151 }
152
setLastError(const String & desc,const bool carryOn)153 void XmlDocument::setLastError (const String& desc, const bool carryOn)
154 {
155 lastError = desc;
156 errorOccurred = ! carryOn;
157 }
158
getFileContents(const String & filename) const159 String XmlDocument::getFileContents (const String& filename) const
160 {
161 if (inputSource != nullptr)
162 {
163 const CarlaScopedPointer<InputStream> in (inputSource->createInputStreamFor (filename.trim().unquoted()));
164
165 if (in != nullptr)
166 return in->readEntireStreamAsString();
167 }
168
169 return String();
170 }
171
readNextChar()172 water_uchar XmlDocument::readNextChar() noexcept
173 {
174 const water_uchar c = input.getAndAdvance();
175
176 if (c == 0)
177 {
178 outOfData = true;
179 --input;
180 }
181
182 return c;
183 }
184
parseDocumentElement(String::CharPointerType textToParse,const bool onlyReadOuterDocumentElement)185 XmlElement* XmlDocument::parseDocumentElement (String::CharPointerType textToParse,
186 const bool onlyReadOuterDocumentElement)
187 {
188 input = textToParse;
189 errorOccurred = false;
190 outOfData = false;
191 needToLoadDTD = true;
192
193 if (textToParse.isEmpty())
194 {
195 lastError = "not enough input";
196 }
197 else if (! parseHeader())
198 {
199 lastError = "malformed header";
200 }
201 else if (! parseDTD())
202 {
203 lastError = "malformed DTD";
204 }
205 else
206 {
207 lastError.clear();
208
209 CarlaScopedPointer<XmlElement> result (readNextElement (! onlyReadOuterDocumentElement));
210
211 if (! errorOccurred)
212 return result.release();
213 }
214
215 return nullptr;
216 }
217
parseHeader()218 bool XmlDocument::parseHeader()
219 {
220 skipNextWhiteSpace();
221
222 if (CharacterFunctions::compareUpTo (input, CharPointer_UTF8 ("<?xml"), 5) == 0)
223 {
224 const String::CharPointerType headerEnd (CharacterFunctions::find (input, CharPointer_UTF8 ("?>")));
225
226 if (headerEnd.isEmpty())
227 return false;
228
229 const String encoding (String (input, headerEnd)
230 .fromFirstOccurrenceOf ("encoding", false, true)
231 .fromFirstOccurrenceOf ("=", false, false)
232 .fromFirstOccurrenceOf ("\"", false, false)
233 .upToFirstOccurrenceOf ("\"", false, false).trim());
234
235 /* If you load an XML document with a non-UTF encoding type, it may have been
236 loaded wrongly.. Since all the files are read via the normal water file streams,
237 they're treated as UTF-8, so by the time it gets to the parser, the encoding will
238 have been lost. Best plan is to stick to utf-8 or if you have specific files to
239 read, use your own code to convert them to a unicode String, and pass that to the
240 XML parser.
241 */
242 CARLA_SAFE_ASSERT_RETURN (encoding.isEmpty() || encoding.startsWithIgnoreCase ("utf-"), false);
243
244 input = headerEnd + 2;
245 skipNextWhiteSpace();
246 }
247
248 return true;
249 }
250
parseDTD()251 bool XmlDocument::parseDTD()
252 {
253 if (CharacterFunctions::compareUpTo (input, CharPointer_UTF8 ("<!DOCTYPE"), 9) == 0)
254 {
255 input += 9;
256 const String::CharPointerType dtdStart (input);
257
258 for (int n = 1; n > 0;)
259 {
260 const water_uchar c = readNextChar();
261
262 if (outOfData)
263 return false;
264
265 if (c == '<')
266 ++n;
267 else if (c == '>')
268 --n;
269 }
270
271 dtdText = String (dtdStart, input - 1).trim();
272 }
273
274 return true;
275 }
276
skipNextWhiteSpace()277 void XmlDocument::skipNextWhiteSpace()
278 {
279 for (;;)
280 {
281 input = input.findEndOfWhitespace();
282
283 if (input.isEmpty())
284 {
285 outOfData = true;
286 break;
287 }
288
289 if (*input == '<')
290 {
291 if (input[1] == '!'
292 && input[2] == '-'
293 && input[3] == '-')
294 {
295 input += 4;
296 const int closeComment = input.indexOf (CharPointer_UTF8 ("-->"));
297
298 if (closeComment < 0)
299 {
300 outOfData = true;
301 break;
302 }
303
304 input += closeComment + 3;
305 continue;
306 }
307
308 if (input[1] == '?')
309 {
310 input += 2;
311 const int closeBracket = input.indexOf (CharPointer_UTF8 ("?>"));
312
313 if (closeBracket < 0)
314 {
315 outOfData = true;
316 break;
317 }
318
319 input += closeBracket + 2;
320 continue;
321 }
322 }
323
324 break;
325 }
326 }
327
readQuotedString(String & result)328 void XmlDocument::readQuotedString (String& result)
329 {
330 const water_uchar quote = readNextChar();
331
332 while (! outOfData)
333 {
334 const water_uchar c = readNextChar();
335
336 if (c == quote)
337 break;
338
339 --input;
340
341 if (c == '&')
342 {
343 readEntity (result);
344 }
345 else
346 {
347 const String::CharPointerType start (input);
348
349 for (;;)
350 {
351 const water_uchar character = *input;
352
353 if (character == quote)
354 {
355 result.appendCharPointer (start, input);
356 ++input;
357 return;
358 }
359 else if (character == '&')
360 {
361 result.appendCharPointer (start, input);
362 break;
363 }
364 else if (character == 0)
365 {
366 setLastError ("unmatched quotes", false);
367 outOfData = true;
368 break;
369 }
370
371 ++input;
372 }
373 }
374 }
375 }
376
readNextElement(const bool alsoParseSubElements)377 XmlElement* XmlDocument::readNextElement (const bool alsoParseSubElements)
378 {
379 XmlElement* node = nullptr;
380
381 skipNextWhiteSpace();
382 if (outOfData)
383 return nullptr;
384
385 if (*input == '<')
386 {
387 ++input;
388 String::CharPointerType endOfToken (XmlIdentifierChars::findEndOfToken (input));
389
390 if (endOfToken == input)
391 {
392 // no tag name - but allow for a gap after the '<' before giving an error
393 skipNextWhiteSpace();
394 endOfToken = XmlIdentifierChars::findEndOfToken (input);
395
396 if (endOfToken == input)
397 {
398 setLastError ("tag name missing", false);
399 return node;
400 }
401 }
402
403 node = new XmlElement (input, endOfToken);
404 input = endOfToken;
405 LinkedListPointer<XmlElement::XmlAttributeNode>::Appender attributeAppender (node->attributes);
406
407 // look for attributes
408 for (;;)
409 {
410 skipNextWhiteSpace();
411
412 const water_uchar c = *input;
413
414 // empty tag..
415 if (c == '/' && input[1] == '>')
416 {
417 input += 2;
418 break;
419 }
420
421 // parse the guts of the element..
422 if (c == '>')
423 {
424 ++input;
425
426 if (alsoParseSubElements)
427 readChildElements (*node);
428
429 break;
430 }
431
432 // get an attribute..
433 if (XmlIdentifierChars::isIdentifierChar (c))
434 {
435 String::CharPointerType attNameEnd (XmlIdentifierChars::findEndOfToken (input));
436
437 if (attNameEnd != input)
438 {
439 const String::CharPointerType attNameStart (input);
440 input = attNameEnd;
441
442 skipNextWhiteSpace();
443
444 if (readNextChar() == '=')
445 {
446 skipNextWhiteSpace();
447
448 const water_uchar nextChar = *input;
449
450 if (nextChar == '"' || nextChar == '\'')
451 {
452 XmlElement::XmlAttributeNode* const newAtt
453 = new XmlElement::XmlAttributeNode (attNameStart, attNameEnd);
454
455 readQuotedString (newAtt->value);
456 attributeAppender.append (newAtt);
457 continue;
458 }
459 }
460 else
461 {
462 setLastError ("expected '=' after attribute '"
463 + String (attNameStart, attNameEnd) + "'", false);
464 return node;
465 }
466 }
467 }
468 else
469 {
470 if (! outOfData)
471 setLastError ("illegal character found in " + node->getTagName() + ": '" + c + "'", false);
472 }
473
474 break;
475 }
476 }
477
478 return node;
479 }
480
readChildElements(XmlElement & parent)481 void XmlDocument::readChildElements (XmlElement& parent)
482 {
483 LinkedListPointer<XmlElement>::Appender childAppender (parent.firstChildElement);
484
485 for (;;)
486 {
487 const String::CharPointerType preWhitespaceInput (input);
488 skipNextWhiteSpace();
489
490 if (outOfData)
491 {
492 setLastError ("unmatched tags", false);
493 break;
494 }
495
496 if (*input == '<')
497 {
498 const water_uchar c1 = input[1];
499
500 if (c1 == '/')
501 {
502 // our close tag..
503 const int closeTag = input.indexOf ((water_uchar) '>');
504
505 if (closeTag >= 0)
506 input += closeTag + 1;
507
508 break;
509 }
510
511 if (c1 == '!' && CharacterFunctions::compareUpTo (input + 2, CharPointer_UTF8 ("[CDATA["), 7) == 0)
512 {
513 input += 9;
514 const String::CharPointerType inputStart (input);
515
516 for (;;)
517 {
518 const water_uchar c0 = *input;
519
520 if (c0 == 0)
521 {
522 setLastError ("unterminated CDATA section", false);
523 outOfData = true;
524 break;
525 }
526 else if (c0 == ']'
527 && input[1] == ']'
528 && input[2] == '>')
529 {
530 childAppender.append (XmlElement::createTextElement (String (inputStart, input)));
531 input += 3;
532 break;
533 }
534
535 ++input;
536 }
537 }
538 else
539 {
540 // this is some other element, so parse and add it..
541 if (XmlElement* const n = readNextElement (true))
542 childAppender.append (n);
543 else
544 break;
545 }
546 }
547 else // must be a character block
548 {
549 input = preWhitespaceInput; // roll back to include the leading whitespace
550 MemoryOutputStream textElementContent;
551 bool contentShouldBeUsed = ! ignoreEmptyTextElements;
552
553 for (;;)
554 {
555 const water_uchar c = *input;
556
557 if (c == '<')
558 {
559 if (input[1] == '!' && input[2] == '-' && input[3] == '-')
560 {
561 input += 4;
562 const int closeComment = input.indexOf (CharPointer_UTF8 ("-->"));
563
564 if (closeComment < 0)
565 {
566 setLastError ("unterminated comment", false);
567 outOfData = true;
568 return;
569 }
570
571 input += closeComment + 3;
572 continue;
573 }
574
575 break;
576 }
577
578 if (c == 0)
579 {
580 setLastError ("unmatched tags", false);
581 outOfData = true;
582 return;
583 }
584
585 if (c == '&')
586 {
587 String entity;
588 readEntity (entity);
589
590 if (entity.startsWithChar ('<') && entity [1] != 0)
591 {
592 const String::CharPointerType oldInput (input);
593 const bool oldOutOfData = outOfData;
594
595 input = entity.getCharPointer();
596 outOfData = false;
597
598 while (XmlElement* n = readNextElement (true))
599 childAppender.append (n);
600
601 input = oldInput;
602 outOfData = oldOutOfData;
603 }
604 else
605 {
606 textElementContent << entity;
607 contentShouldBeUsed = contentShouldBeUsed || entity.containsNonWhitespaceChars();
608 }
609 }
610 else
611 {
612 for (;; ++input)
613 {
614 water_uchar nextChar = *input;
615
616 if (nextChar == '\r')
617 {
618 nextChar = '\n';
619
620 if (input[1] == '\n')
621 continue;
622 }
623
624 if (nextChar == '<' || nextChar == '&')
625 break;
626
627 if (nextChar == 0)
628 {
629 setLastError ("unmatched tags", false);
630 outOfData = true;
631 return;
632 }
633
634 textElementContent.appendUTF8Char (nextChar);
635 contentShouldBeUsed = contentShouldBeUsed || ! CharacterFunctions::isWhitespace (nextChar);
636 }
637 }
638 }
639
640 if (contentShouldBeUsed)
641 childAppender.append (XmlElement::createTextElement (textElementContent.toUTF8()));
642 }
643 }
644 }
645
readEntity(String & result)646 void XmlDocument::readEntity (String& result)
647 {
648 // skip over the ampersand
649 ++input;
650
651 if (input.compareIgnoreCaseUpTo (CharPointer_UTF8 ("amp;"), 4) == 0)
652 {
653 input += 4;
654 result += '&';
655 }
656 else if (input.compareIgnoreCaseUpTo (CharPointer_UTF8 ("quot;"), 5) == 0)
657 {
658 input += 5;
659 result += '"';
660 }
661 else if (input.compareIgnoreCaseUpTo (CharPointer_UTF8 ("apos;"), 5) == 0)
662 {
663 input += 5;
664 result += '\'';
665 }
666 else if (input.compareIgnoreCaseUpTo (CharPointer_UTF8 ("lt;"), 3) == 0)
667 {
668 input += 3;
669 result += '<';
670 }
671 else if (input.compareIgnoreCaseUpTo (CharPointer_UTF8 ("gt;"), 3) == 0)
672 {
673 input += 3;
674 result += '>';
675 }
676 else if (*input == '#')
677 {
678 int charCode = 0;
679 ++input;
680
681 if (*input == 'x' || *input == 'X')
682 {
683 ++input;
684 int numChars = 0;
685
686 while (input[0] != ';')
687 {
688 const int hexValue = CharacterFunctions::getHexDigitValue (input[0]);
689
690 if (hexValue < 0 || ++numChars > 8)
691 {
692 setLastError ("illegal escape sequence", true);
693 break;
694 }
695
696 charCode = (charCode << 4) | hexValue;
697 ++input;
698 }
699
700 ++input;
701 }
702 else if (input[0] >= '0' && input[0] <= '9')
703 {
704 int numChars = 0;
705
706 while (input[0] != ';')
707 {
708 if (++numChars > 12)
709 {
710 setLastError ("illegal escape sequence", true);
711 break;
712 }
713
714 charCode = charCode * 10 + ((int) input[0] - '0');
715 ++input;
716 }
717
718 ++input;
719 }
720 else
721 {
722 setLastError ("illegal escape sequence", true);
723 result += '&';
724 return;
725 }
726
727 result << (water_uchar) charCode;
728 }
729 else
730 {
731 const String::CharPointerType entityNameStart (input);
732 const int closingSemiColon = input.indexOf ((water_uchar) ';');
733
734 if (closingSemiColon < 0)
735 {
736 outOfData = true;
737 result += '&';
738 }
739 else
740 {
741 input += closingSemiColon + 1;
742
743 result += expandExternalEntity (String (entityNameStart, (size_t) closingSemiColon));
744 }
745 }
746 }
747
expandEntity(const String & ent)748 String XmlDocument::expandEntity (const String& ent)
749 {
750 if (ent.equalsIgnoreCase ("amp")) return String::charToString ('&');
751 if (ent.equalsIgnoreCase ("quot")) return String::charToString ('"');
752 if (ent.equalsIgnoreCase ("apos")) return String::charToString ('\'');
753 if (ent.equalsIgnoreCase ("lt")) return String::charToString ('<');
754 if (ent.equalsIgnoreCase ("gt")) return String::charToString ('>');
755
756 if (ent[0] == '#')
757 {
758 const water_uchar char1 = ent[1];
759
760 if (char1 == 'x' || char1 == 'X')
761 return String::charToString (static_cast<water_uchar> (ent.substring (2).getHexValue32()));
762
763 if (char1 >= '0' && char1 <= '9')
764 return String::charToString (static_cast<water_uchar> (ent.substring (1).getIntValue()));
765
766 setLastError ("illegal escape sequence", false);
767 return String::charToString ('&');
768 }
769
770 return expandExternalEntity (ent);
771 }
772
expandExternalEntity(const String & entity)773 String XmlDocument::expandExternalEntity (const String& entity)
774 {
775 if (needToLoadDTD)
776 {
777 if (dtdText.isNotEmpty())
778 {
779 dtdText = dtdText.trimCharactersAtEnd (">");
780 tokenisedDTD.addTokens (dtdText, true);
781
782 if (tokenisedDTD [tokenisedDTD.size() - 2].equalsIgnoreCase ("system")
783 && tokenisedDTD [tokenisedDTD.size() - 1].isQuotedString())
784 {
785 const String fn (tokenisedDTD [tokenisedDTD.size() - 1]);
786
787 tokenisedDTD.clear();
788 tokenisedDTD.addTokens (getFileContents (fn), true);
789 }
790 else
791 {
792 tokenisedDTD.clear();
793 const int openBracket = dtdText.indexOfChar ('[');
794
795 if (openBracket > 0)
796 {
797 const int closeBracket = dtdText.lastIndexOfChar (']');
798
799 if (closeBracket > openBracket)
800 tokenisedDTD.addTokens (dtdText.substring (openBracket + 1,
801 closeBracket), true);
802 }
803 }
804
805 for (int i = tokenisedDTD.size(); --i >= 0;)
806 {
807 if (tokenisedDTD[i].startsWithChar ('%')
808 && tokenisedDTD[i].endsWithChar (';'))
809 {
810 const String parsed (getParameterEntity (tokenisedDTD[i].substring (1, tokenisedDTD[i].length() - 1)));
811 StringArray newToks;
812 newToks.addTokens (parsed, true);
813
814 tokenisedDTD.remove (i);
815
816 for (int j = newToks.size(); --j >= 0;)
817 tokenisedDTD.insert (i, newToks[j]);
818 }
819 }
820 }
821
822 needToLoadDTD = false;
823 }
824
825 for (int i = 0; i < tokenisedDTD.size(); ++i)
826 {
827 if (tokenisedDTD[i] == entity)
828 {
829 if (tokenisedDTD[i - 1].equalsIgnoreCase ("<!entity"))
830 {
831 String ent (tokenisedDTD [i + 1].trimCharactersAtEnd (">").trim().unquoted());
832
833 // check for sub-entities..
834 int ampersand = ent.indexOfChar ('&');
835
836 while (ampersand >= 0)
837 {
838 const int semiColon = ent.indexOf (i + 1, ";");
839
840 if (semiColon < 0)
841 {
842 setLastError ("entity without terminating semi-colon", false);
843 break;
844 }
845
846 const String resolved (expandEntity (ent.substring (i + 1, semiColon)));
847
848 ent = ent.substring (0, ampersand)
849 + resolved
850 + ent.substring (semiColon + 1);
851
852 ampersand = ent.indexOfChar (semiColon + 1, '&');
853 }
854
855 return ent;
856 }
857 }
858 }
859
860 setLastError ("unknown entity", true);
861
862 return entity;
863 }
864
getParameterEntity(const String & entity)865 String XmlDocument::getParameterEntity (const String& entity)
866 {
867 for (int i = 0; i < tokenisedDTD.size(); ++i)
868 {
869 if (tokenisedDTD[i] == entity
870 && tokenisedDTD [i - 1] == "%"
871 && tokenisedDTD [i - 2].equalsIgnoreCase ("<!entity"))
872 {
873 const String ent (tokenisedDTD [i + 1].trimCharactersAtEnd (">"));
874
875 if (ent.equalsIgnoreCase ("system"))
876 return getFileContents (tokenisedDTD [i + 2].trimCharactersAtEnd (">"));
877
878 return ent.trim().unquoted();
879 }
880 }
881
882 return entity;
883 }
884
885 }
886