001/* A parser for PlotML (Plot Markup Language) supporting PlotBoxML commands.
002
003 Copyright (c) 1998-2016 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.plot.plotml;
029
030import java.io.BufferedReader;
031import java.io.InputStream;
032import java.io.InputStreamReader;
033import java.io.Reader;
034import java.io.StringReader;
035import java.net.URL;
036import java.util.Hashtable;
037import java.util.Stack;
038
039import com.microstar.xml.HandlerBase;
040import com.microstar.xml.XmlException;
041import com.microstar.xml.XmlParser;
042
043import ptolemy.plot.PlotBoxInterface;
044
045///////////////////////////////////////////////////////////////////
046//// PlotBoxMLParser
047
048/**
049 This class constructs a plot from specifications
050 in PlotML (Plot Markup Language), which is an XML language.
051 This class supports only the subset that applies to the PlotBox base class.
052 It ignores all other elements in the DTD.
053 The class contains an instance of the Microstar Ælfred XML
054 parser and implements callback methods to interpret the parsed XML.
055 The way to use this class is to construct it with a reference to
056 a PlotBox object and then call its parse() method.
057
058 @author Edward A. Lee
059 @version $Id$
060 @since Ptolemy II 0.4
061 @Pt.ProposedRating Red (eal)
062 @Pt.AcceptedRating Red (cxh)
063 */
064public class PlotBoxMLParser extends HandlerBase {
065    /** Construct an parser to parse commands for the specified plot object.
066     *  @param plot The plot object to which to apply the commands.
067     */
068    public PlotBoxMLParser(PlotBoxInterface plot) {
069        super();
070        _plot = plot;
071    }
072
073    /** Protected constructor allows derived classes to set _plot
074     *  differently.
075     */
076    protected PlotBoxMLParser() {
077    }
078
079    ///////////////////////////////////////////////////////////////////
080    ////                         public methods                    ////
081
082    /** Handle an attribute assignment that is part of an XML element.
083     *  This method is called prior to the corresponding startElement()
084     *  call, so it simply accumulates attributes in a hashtable for
085     *  use by startElement().
086     *  @param name The name of the attribute.
087     *  @param value The value of the attribute, or null if the attribute
088     *   is <code>#IMPLIED</code> and not specified.
089     *  @param specified True if the value is specified, false if the
090     *   value comes from the default value in the DTD rather than from
091     *   the XML file.
092     *  @exception XmlException If the name or value is null.
093     */
094    @Override
095    public void attribute(String name, String value, boolean specified)
096            throws XmlException {
097        if (name == null) {
098            throw new XmlException("Attribute has no name",
099                    _currentExternalEntity(), _parser.getLineNumber(),
100                    _parser.getColumnNumber());
101        }
102
103        // NOTE: value may be null if attribute default is #IMPLIED.
104        if (value != null) {
105            _attributes.put(name, value);
106        }
107    }
108
109    /** Handle character data.  In this implementation, the
110     *  character data is accumulated in a buffer until the
111     *  end element.
112     *  &AElig;lfred will call this method once for each chunk of
113     *  character data found in the contents of elements.  Note that
114     *  the parser may break up a long sequence of characters into
115     *  smaller chunks and call this method once for each chunk.
116     *  @param chars The character data.
117     *  @param offset The starting position in the array.
118     *  @param length The number of characters available.
119     */
120    @Override
121    public void charData(char[] chars, int offset, int length) {
122        _currentCharData.append(chars, offset, length);
123    }
124
125    /** End the document.  In this implementation, do nothing.
126     *  &AElig;lfred will call this method once, when it has
127     *  finished parsing the XML document.
128     *  It is guaranteed that this will be the last method called.
129     */
130    @Override
131    public void endDocument() throws Exception {
132    }
133
134    /** End an element. For most elements this method
135     *  calls the appropriate PlotBox method.
136     *  &AElig;lfred will call this method at the end of each element
137     *  (including EMPTY elements).
138     *  @param elementName The element type name.
139     *  @exception Exception If thrown while invoking a method on the
140     *  Plot object.
141     */
142    @Override
143    public void endElement(String elementName) throws Exception {
144        // NOTE: The elements are alphabetical below...
145        if (elementName.equals("caption")) {
146            _plot.addCaptionLine(_currentCharData.toString());
147            _plot.setGrid(false);
148        } else if (elementName.equals("noGrid")) {
149            _plot.setGrid(false);
150        } else if (elementName.equals("noColor")) {
151            _plot.setColor(false);
152        } else if (elementName.equals("title")) {
153            _plot.setTitle(_currentCharData.toString());
154        } else if (elementName.equals("wrap")) {
155            _plot.setWrap(true);
156        } else if (elementName.equals("xLabel")) {
157            _plot.setXLabel(_currentCharData.toString());
158        } else if (elementName.equals("xLog")) {
159            _plot.setXLog(true);
160            _plot.setXLabel(_currentCharData.toString());
161            // xRange and yRange are dealt with in startElement().
162        } else if (elementName.equals("yLabel")) {
163            _plot.setYLabel(_currentCharData.toString());
164        } else if (elementName.equals("yLog")) {
165            _plot.setYLog(true);
166        }
167    }
168
169    /** Indicate a fatal XML parsing error.
170     *  &AElig;lfred will call this method whenever it encounters
171     *  a serious error.  This method simply throws an XmlException.
172     *  @param message The error message.
173     *  @param systemID The URI of the entity that caused the error.
174     *  @param line The approximate line number of the error.
175     *  @param column The approximate column number of the error.
176     *  @exception XmlException If called.
177     */
178    @Override
179    public void error(String message, String systemID, int line, int column)
180            throws XmlException {
181        throw new XmlException(message, _currentExternalEntity(), line, column);
182    }
183
184    /** Parse the given stream as a PlotML file.
185     *  For example, an applet might use this method as follows:
186     *  <pre>
187     *     PlotBoxMLParser parser = new PlotBoxMLParser();
188     *     URL docBase = getDocumentBase();
189     *     URL xmlFile = new URL(docBase, modelURL);
190     *     parser.parse(xmlFile.openStream());
191     *  </pre>
192     *  A variety of exceptions might be thrown if the parsed
193     *  data does not represent a valid PlotML file.
194     *  @param base The base URL for relative references, or null if
195     *   there is none.
196     *  @param input The stream from which to read XML.
197     *  @exception Exception If the parser fails.
198     */
199    public void parse(URL base, InputStream input) throws Exception {
200        parse(base, new InputStreamReader(input,
201                java.nio.charset.Charset.defaultCharset()));
202    }
203
204    /** Parse the given stream as a PlotML file.
205     *  A variety of exceptions might be thrown if the parsed
206     *  data does not represent a valid PlotML file.
207     *  @param base The base URL for relative references, or null if
208     *   there is none.
209     *  @param reader The stream from which to read XML.
210     *  @exception Exception If the parser fails.
211     */
212    public void parse(URL base, Reader reader) throws Exception {
213        _parser.setHandler(this);
214
215        Reader buffered = new BufferedReader(reader);
216
217        if (base == null) {
218            _parser.parse(null, null, buffered);
219        } else {
220            _parser.parse(base.toExternalForm(), null, buffered);
221        }
222    }
223
224    /** Parse the given text as PlotML.
225     *  A variety of exceptions might be thrown if the parsed
226     *  data does not represent valid PlotML data.
227     *  @param base The base URL for relative references, or null if
228     *   there is none.
229     *  @param text The PlotML data.
230     *  @exception Exception If the parser fails.
231     */
232    public void parse(URL base, String text) throws Exception {
233        parse(base, new StringReader(text));
234    }
235
236    /** Resolve an external entity. If the first argument is the
237     *  name of the PlotML PUBLIC DTD ("-//UC Berkeley//DTD PlotML 1//EN"),
238     *  then return a StringReader
239     *  that will read the locally cached version of this DTD
240     *  (the public variable PlotML_DTD_1). Otherwise, return null,
241     *  which has the effect of deferring to &AElig;lfred for
242     *  resolution of the URI.  Derived classes may return a
243     *  a modified URI (a string), an InputStream, or a Reader.
244     *  In the latter two cases, the input character stream is
245     *  provided.
246     *  @param publicID The public identifier, or null if none was supplied.
247     *  @param systemID The system identifier.
248     *  @return Null, indicating to use the default system identifier.
249     */
250    @Override
251    public Object resolveEntity(String publicID, String systemID) {
252        if (publicID != null
253                && publicID.equals("-//UC Berkeley//DTD PlotML 1//EN")) {
254            // This is the generic MoML DTD.
255            return new StringReader(PlotML_DTD_1);
256        } else {
257            return null;
258        }
259    }
260
261    /** Start a document.  This method is called just before the parser
262     *  attempts to read the first entity (the root of the document).
263     *  It is guaranteed that this will be the first method called.
264     */
265    @Override
266    public void startDocument() {
267        _attributes = new Hashtable();
268    }
269
270    /** Start an element.
271     *  This is called at the beginning of each XML
272     *  element.  By the time it is called, all of the attributes
273     *  for the element will already have been reported using the
274     *  attribute() method.  Unrecognized elements are ignored.
275     *  @param elementName The element type name.
276     *  @exception XmlException If the element produces an error
277     *   in constructing the model.
278     */
279    @Override
280    public void startElement(String elementName) throws XmlException {
281        try {
282            // NOTE: The elements are alphabetical below...
283            if (elementName.equals("caption")) {
284                _currentCharData = new StringBuffer();
285            } else if (elementName.equals("size")) {
286                String spec = (String) _attributes.get("height");
287                _checkForNull(spec, "No height argument for element \"size\"");
288
289                // NOTE: Do not use parseDouble() to maintain Java 1.1
290                // compatibility.
291                int height = Integer.parseInt(spec);
292
293                spec = (String) _attributes.get("width");
294                _checkForNull(spec, "No width argument for element \"size\"");
295
296                // NOTE: Do not use parseDouble() to maintain Java 1.1
297                // compatibility.
298                int width = Integer.parseInt(spec);
299
300                _plot.setSize(width, height);
301            } else if (elementName.equals("tick")) {
302                String label = (String) _attributes.get("label");
303                _checkForNull(label, "No label for element \"tick\"");
304
305                String spec = (String) _attributes.get("position");
306                _checkForNull(spec, "No position for element \"tick\"");
307
308                // NOTE: Do not use parseDouble() to maintain Java 1.1
309                // compatibility.
310                double position = Double.parseDouble(spec);
311
312                if (_xtick) {
313                    _plot.addXTick(label, position);
314                } else {
315                    _plot.addYTick(label, position);
316                }
317            } else if (elementName.equals("title")) {
318                _currentCharData = new StringBuffer();
319            } else if (elementName.equals("xLabel")) {
320                _currentCharData = new StringBuffer();
321            } else if (elementName.equals("xRange")) {
322                String spec = (String) _attributes.get("min");
323                _checkForNull(spec, "No min argument for element \"xRange\"");
324
325                double min = Double.parseDouble(spec);
326
327                spec = (String) _attributes.get("max");
328                _checkForNull(spec, "No max argument for element \"xRange\"");
329
330                double max = Double.parseDouble(spec);
331
332                _plot.setXRange(min, max);
333            } else if (elementName.equals("xTicks")) {
334                _xtick = true;
335            } else if (elementName.equals("yLabel")) {
336                _currentCharData = new StringBuffer();
337            } else if (elementName.equals("yRange")) {
338                String spec = (String) _attributes.get("min");
339                _checkForNull(spec, "No min argument for element \"yRange\"");
340
341                double min = Double.parseDouble(spec);
342
343                spec = (String) _attributes.get("max");
344                _checkForNull(spec, "No max argument for element \"yRange\"");
345
346                double max = Double.parseDouble(spec);
347
348                _plot.setYRange(min, max);
349            } else if (elementName.equals("yTicks")) {
350                _xtick = false;
351            }
352        } catch (Throwable ex) {
353            if (ex instanceof XmlException) {
354                throw (XmlException) ex;
355            } else {
356                String msg = "XML element \"" + elementName
357                        + "\" triggers exception:\n  " + ex.toString();
358                throw new XmlException(msg, _currentExternalEntity(),
359                        _parser.getLineNumber(), _parser.getColumnNumber());
360            }
361        }
362
363        _attributes.clear();
364    }
365
366    /** Handle the start of an external entity.  This pushes the stack so
367     *  that error reporting correctly reports the external entity that
368     *  causes the error.
369     *  @param systemID The URI for the external entity.
370     */
371    @Override
372    public void startExternalEntity(String systemID) {
373        _externalEntities.push(systemID);
374    }
375
376    ///////////////////////////////////////////////////////////////////
377    ////                         public members                    ////
378
379    /** The standard PlotML DTD, represented as a string.  This is used
380     *  to parse PlotML data when a compatible PUBLIC DTD is specified.
381     */
382    public static final String PlotML_DTD_1 = "<!ELEMENT plot (barGraph | bin | caption | dataset | default | noColor | noGrid | size | title | wrap | xLabel | xLog | xRange | xTicks | yLabel | yLog | yRange | yTicks)*><!ELEMENT barGraph EMPTY><!ATTLIST barGraph width CDATA #IMPLIED offset CDATA #IMPLIED><!ELEMENT bin EMPTY><!ATTLIST bin width CDATA #IMPLIED offset CDATA #IMPLIED><!ELEMENT caption (#PCDATA)><!ELEMENT dataset (m | move | p | point)*><!ATTLIST dataset connected (yes | no) #IMPLIED lineStyle (solid | dotted | dashed | dotdashed | dotdotdashed) #IMPLIED marks (none | dots | points | various | pixels | bigdots) #IMPLIED name CDATA #IMPLIED stems (yes | no) #IMPLIED><!ELEMENT default EMPTY><!ATTLIST default connected (yes | no) \"yes\" marks (none | dots | points | various | pixels | bigdots) \"none\" stems (yes | no) \"no\"><!ELEMENT noColor EMPTY><!ELEMENT noGrid EMPTY><!ELEMENT reuseDatasets EMPTY><!ELEMENT size EMPTY><!ATTLIST size height CDATA #REQUIRED width CDATA #REQUIRED><!ELEMENT title (#PCDATA)><!ELEMENT wrap EMPTY><!ELEMENT xLabel (#PCDATA)><!ELEMENT xLog EMPTY><!ELEMENT xRange EMPTY><!ATTLIST xRange min CDATA #REQUIRED max CDATA #REQUIRED><!ELEMENT xTicks (tick)+><!ELEMENT yLabel (#PCDATA)><!ELEMENT yLog EMPTY><!ELEMENT yRange EMPTY><!ATTLIST yRange min CDATA #REQUIRED max CDATA #REQUIRED><!ELEMENT yTicks (tick)+><!ELEMENT tick EMPTY><!ATTLIST tick label CDATA #REQUIRED position CDATA #REQUIRED><!ELEMENT m EMPTY><!ATTLIST m x CDATA #IMPLIED y CDATA #REQUIRED lowErrorBar CDATA #IMPLIED highErrorBar CDATA #IMPLIED><!ELEMENT move EMPTY><!ATTLIST move x CDATA #IMPLIED y CDATA #REQUIRED lowErrorBar CDATA #IMPLIED highErrorBar CDATA #IMPLIED><!ELEMENT p EMPTY><!ATTLIST p x CDATA #IMPLIED y CDATA #REQUIRED lowErrorBar CDATA #IMPLIED highErrorBar CDATA #IMPLIED><!ELEMENT point EMPTY><!ATTLIST point x CDATA #IMPLIED y CDATA #REQUIRED lowErrorBar CDATA #IMPLIED highErrorBar CDATA #IMPLIED>";
383
384    // NOTE: The master file for the above DTD is at
385    // $PTII/ptolemy/plot/plotml/plotml.dtd.  If modified, it needs to be also
386    // updated at ptweb/archive/plotml.dtd.
387    ///////////////////////////////////////////////////////////////////
388    ////                         protected methods                 ////
389
390    /** If the argument is null, throw an exception with the given message.
391     *  @param object The reference to check for null.
392     *  @param message The message to issue if the reference is null.
393     *  @exception XmlException If the object argument is null.
394     */
395    protected void _checkForNull(Object object, String message)
396            throws XmlException {
397        if (object == null) {
398            throw new XmlException(message, _currentExternalEntity(),
399                    _parser.getLineNumber(), _parser.getColumnNumber());
400        }
401    }
402
403    /** Get the the URI for the current external entity.
404     *  @return A string giving the URI of the external entity being read,
405     *   or null if none.
406     */
407    protected String _currentExternalEntity() {
408        return (String) _externalEntities.peek();
409    }
410
411    ///////////////////////////////////////////////////////////////////
412    ////                         protected members                 ////
413    // NOTE: Do not use HashMap here to maintain Java 1.1 compatibility.
414
415    /** Attributes associated with an entity. */
416    protected Hashtable _attributes;
417
418    /** The current character data for the current element. */
419    protected StringBuffer _currentCharData = new StringBuffer();
420
421    /** The parser. */
422    protected XmlParser _parser = new XmlParser();
423
424    /** The plot object to which to apply commands. */
425    protected PlotBoxInterface _plot;
426
427    ///////////////////////////////////////////////////////////////////
428    ////                         private members                   ////
429
430    // The external entities being parsed.
431    private Stack _externalEntities = new Stack();
432
433    // Indicator of whether we are parsing x ticks or y ticks.
434    private boolean _xtick;
435}