Improve XML parsing to handle attributes as well.
[cfb.git] / prod / net / jaekl / qd / xml / ParseResult.java
1 // Copyright (C) 2004, 2015 Christian Jaekl
2
3 // Abstract class representing the result of parsing an XML Element.
4 // A class derived from this one will know how to parse a subtree inside an XML file, and 
5 // will contain the result of that parse within itself when the parse has completed.
6 //
7 // Note that this code will need to be augmented and fixed if XML namespace support is desired.
8
9 package net.jaekl.qd.xml;
10
11 import java.io.PrintWriter;
12 import java.util.ArrayList;
13 import java.util.HashMap;
14 import java.util.HashSet;
15 import java.util.Iterator;
16 import java.util.Stack;
17
18 import org.xml.sax.Attributes;
19 import org.xml.sax.helpers.AttributesImpl;
20
21 public abstract class ParseResult
22 {
23         public static final String TAG_FIELD_NAME = "TAG";
24         
25         Stack<CurrentInfo> m_current;                                                           // Name of the element that we're currently inside
26         StringBuilder m_chars;                                                                          // character content of m_current.peek()
27         ArrayList<ParseResult> m_childParsers;                                          // Set of all child parsers
28         boolean m_haveSeenMyTag;                                                                        // Have I encountered my own (root) tag yet?
29
30         String m_tagName;                                                                                       // Name of the (root) element tag that I'm parsing
31         HashSet<String> m_internal;                                                                     // Tags that we will store as members of this class instance
32         HashMap<String,Class<? extends ParseResult>> m_external;        // Tags that we will store as child ParseResult-derived objects
33
34         // Information about the "current" tag, which we'll keep on the m_current stack.
35         static class CurrentInfo {
36                 String m_tagName;
37                 Attributes m_attr;
38                 
39                 public CurrentInfo(String tagName, Attributes attr) {
40                         m_tagName = tagName;
41                         m_attr = new AttributesImpl(attr);
42                 }
43                 
44                 public String getTagName() { return m_tagName; }
45                 public final Attributes getAttributes() { return m_attr; }
46         }
47         
48         @SuppressWarnings("unchecked")
49         public ParseResult(String tagName, String[] internalMemberTags, Object[][] externalParserTags)
50         {
51                 m_current = new Stack<CurrentInfo>();
52                 m_chars = new StringBuilder();
53                 m_childParsers = new ArrayList<ParseResult>();
54                 m_haveSeenMyTag = false;
55                 
56                 m_tagName = tagName;
57                 m_internal = new HashSet<String>();
58                 m_external = new HashMap<String, Class<? extends ParseResult>>();
59
60                 for (String internalTag : internalMemberTags) {
61                         m_internal.add(internalTag);
62                 }
63
64                 for (int idx = 0; idx < externalParserTags.length; ++idx) {
65                         String externalTag = (String)externalParserTags[idx][0];
66                         Class<? extends ParseResult>  parserClass = (Class<? extends ParseResult>)externalParserTags[idx][1];
67                         m_external.put(externalTag, parserClass);
68                 }
69         }
70
71         public abstract void endContents(String uri, String localName, String qName, String chars) throws XmlParseException;
72         public abstract void endExternal(String uri, String localName, String qName) throws XmlParseException;
73
74         // Called once for this tag itself
75         public void handleMainAttributes(Attributes attr) throws MissingAttributeException {
76                 // Default action is to do nothing.
77                 // Override this if you want to process tag attributes.
78         }
79         
80         // Called once for each internally-handled subtag
81         public void handleInternalAttributes(String tagName, Attributes attr) throws MissingAttributeException {
82                 // Default action is to do nothing.
83                 // Override this if you want to process tag attributes.
84         }
85         
86         public String getTagName() { return m_tagName; }
87
88         public void characters(char[] ch, int start, int length) throws XmlParseException
89         {
90                 m_chars.append(ch, start, length);
91         }
92         
93         // Dump human-readable text describing this tag and its children.
94         // Default implemenation:  print the tag name (only)
95         public void dump(PrintWriter pw, int indent) {
96                 String margin = String.format("%"+indent+"s", "");
97                 pw.println(margin + getTagName());
98         }
99         
100         protected ParseResult[] collectParsedChildren(Class<? extends ParseResult> cls) {
101                 ArrayList<ParseResult> collection = new ArrayList<ParseResult>();
102                 Iterator<ParseResult> iter = m_childParsers.iterator();
103                 while (iter.hasNext()) {
104                         ParseResult pr = iter.next();
105                         if (pr.getClass().isAssignableFrom(cls)) {
106                                 collection.add(pr);
107                                 iter.remove();
108                         }
109                 }
110                 
111                 ParseResult[] result = new ParseResult[collection.size()];
112                 return collection.toArray(result);
113         }
114         
115         // returns true if this ParseResult's context has ended with this endElement() call
116         protected boolean endElement(String uri, String localName, String qName) throws XmlParseException
117         {
118                 assert (null != localName);
119                 
120                 boolean isInternal = m_internal.contains(localName);
121
122                 if (! m_haveSeenMyTag) {
123                         // We're in some unrecognised prologue.  Ignore it and move on.
124                         return false;
125                 }
126                 
127                 if (m_tagName.equals(localName)) {
128                         validate();
129                         return true;
130                 }
131                 
132                 if (!isInternal) {
133                         // Unrecognized tag.  Ignore it.
134                         return false;
135                 }
136                 
137                 CurrentInfo info = m_current.pop();
138                 String tag = info.getTagName();
139                 if ( ! tag.equals(localName) ) {
140                         throw new MismatchedTagsException(tag, localName);
141                 }
142                 
143                 String chars = m_chars.toString();
144                 endContents(uri, localName, qName, chars);
145                 
146                 return false;
147         }
148         
149         // returns either itself, or a new ParseResult-derived object, whichever should handle parsing the inside of this element
150         public ParseResult startElement(String uri, String localName, String qName, Attributes attrs) 
151                         throws XmlParseException
152         {
153                 assert (null != localName);
154
155                 m_chars.setLength(0);
156                 
157                 if (! m_haveSeenMyTag) {
158                         // Have we opened our own (root) tag yet?
159                         
160                         if (m_tagName.equals(localName)) {
161                                 m_haveSeenMyTag = true;
162                                 handleMainAttributes(attrs);
163                                 return this;
164                         }
165                         else {
166                                 // One of two things has happened here.
167                                 // Either (a) we've got some sort of wrapper here, and have not yet reach our own tag, 
168                                 //     or (b) we're parsing XML that doesn't match expectations.
169                                 // In either case, we're going to ignore this tag, and scan forward looking for our own root.
170                                 return this;
171                         }
172                 }
173
174                 if (m_internal.contains(localName)) {
175                         handleInternalAttributes(localName, attrs);
176                         CurrentInfo info = new CurrentInfo(localName, attrs);
177                         m_current.push(info);
178                         return this;
179                 }
180
181                 Class<? extends ParseResult> parserClass = m_external.get(localName);
182                 if (null != parserClass) {
183                         try {
184                                 ParseResult childParser = (ParseResult) parserClass.newInstance();
185                                 m_childParsers.add(childParser);
186                                 return childParser.startElement(uri, localName, qName, attrs);
187                         }
188                         catch (IllegalAccessException iae) {
189                                 throw new XmlParseException(iae);
190                         }
191                         catch (InstantiationException ie) {
192                                 throw new XmlParseException(ie);
193                         }
194                 }
195                 
196                 // Not a recognized tag.  Ignore it, rather than complaining. 
197                 return this;
198         }
199         
200         public void validate() throws XmlParseException
201         {
202                 // Default implementation is a no-op.
203                 // Override if you want to validate on endElement()
204         }
205         
206         protected String getRequiredAttr(String tagName, Attributes attr, String attrName)
207                 throws MissingAttributeException
208         {
209                 String result = attr.getValue(attrName);
210                 if (null == result) {
211                         throw new MissingAttributeException(tagName, attrName);
212                 }
213                 return result;
214         }
215         
216         protected String getOptionalAttr(Attributes attr, String attrName, String defaultValue)
217         {
218                 String value = attr.getValue(attrName);
219                 if (null == value) {
220                         return defaultValue;
221                 }
222                 return value;
223         }
224
225 }
226