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 * Æ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 * Æ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 * Æ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 * Æ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 Æ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}