001/* 002 * Copyright 2006 - 2013 003 * Stefan Balev <stefan.balev@graphstream-project.org> 004 * Julien Baudry <julien.baudry@graphstream-project.org> 005 * Antoine Dutot <antoine.dutot@graphstream-project.org> 006 * Yoann Pigné <yoann.pigne@graphstream-project.org> 007 * Guilhelm Savin <guilhelm.savin@graphstream-project.org> 008 * 009 * This file is part of GraphStream <http://graphstream-project.org>. 010 * 011 * GraphStream is a library whose purpose is to handle static or dynamic 012 * graph, create them from scratch, file or any source and display them. 013 * 014 * This program is free software distributed under the terms of two licenses, the 015 * CeCILL-C license that fits European law, and the GNU Lesser General Public 016 * License. You can use, modify and/ or redistribute the software under the terms 017 * of the CeCILL-C license as circulated by CEA, CNRS and INRIA at the following 018 * URL <http://www.cecill.info> or under the terms of the GNU LGPL as published by 019 * the Free Software Foundation, either version 3 of the License, or (at your 020 * option) any later version. 021 * 022 * This program is distributed in the hope that it will be useful, but WITHOUT ANY 023 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 024 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 025 * 026 * You should have received a copy of the GNU Lesser General Public License 027 * along with this program. If not, see <http://www.gnu.org/licenses/>. 028 * 029 * The fact that you are presently reading this means that you have had 030 * knowledge of the CeCILL-C and LGPL licenses and that you accept their terms. 031 */ 032package org.graphstream.stream.file; 033 034import java.io.FileReader; 035import java.io.IOException; 036import java.io.InputStream; 037import java.io.InputStreamReader; 038import java.io.Reader; 039import java.net.URL; 040import java.util.EnumMap; 041import java.util.Iterator; 042import java.util.Stack; 043 044import javax.xml.stream.FactoryConfigurationError; 045import javax.xml.stream.XMLEventReader; 046import javax.xml.stream.XMLInputFactory; 047import javax.xml.stream.XMLStreamConstants; 048import javax.xml.stream.XMLStreamException; 049import javax.xml.stream.events.Attribute; 050import javax.xml.stream.events.StartElement; 051import javax.xml.stream.events.XMLEvent; 052 053import org.graphstream.stream.SourceBase; 054 055/** 056 * Base for XML-based file format. It uses an xml events stream ( 057 * {@link javax.xml.streams}). One who want to define a new xml-based fiel 058 * source has to define actions after the document start and before the document 059 * end. The {@link #nextEvents()}, called between start and end, has to be 060 * defined too. 061 * 062 * @author Guilhelm Savin 063 * 064 */ 065public abstract class FileSourceXML extends SourceBase implements FileSource, 066 XMLStreamConstants { 067 068 /** 069 * XML events stream. Should not be used directly but with 070 * {@link #getNextEvent()}. 071 */ 072 protected XMLEventReader reader; 073 /* 074 * Used to allow 'pushback' of events. 075 */ 076 private Stack<XMLEvent> events; 077 078 protected FileSourceXML() { 079 events = new Stack<XMLEvent>(); 080 } 081 082 /* 083 * (non-Javadoc) 084 * 085 * @see org.graphstream.stream.file.FileSource#readAll(java.lang.String) 086 */ 087 public void readAll(String fileName) throws IOException { 088 readAll(new FileReader(fileName)); 089 } 090 091 /* 092 * (non-Javadoc) 093 * 094 * @see org.graphstream.stream.file.FileSource#readAll(java.net.URL) 095 */ 096 public void readAll(URL url) throws IOException { 097 readAll(url.openStream()); 098 } 099 100 /* 101 * (non-Javadoc) 102 * 103 * @see org.graphstream.stream.file.FileSource#readAll(java.io.InputStream) 104 */ 105 public void readAll(InputStream stream) throws IOException { 106 readAll(new InputStreamReader(stream)); 107 } 108 109 /* 110 * (non-Javadoc) 111 * 112 * @see org.graphstream.stream.file.FileSource#readAll(java.io.Reader) 113 */ 114 public void readAll(Reader reader) throws IOException { 115 begin(reader); 116 while (nextEvents()) 117 ; 118 end(); 119 } 120 121 /* 122 * (non-Javadoc) 123 * 124 * @see org.graphstream.stream.file.FileSource#begin(java.lang.String) 125 */ 126 public void begin(String fileName) throws IOException { 127 begin(new FileReader(fileName)); 128 } 129 130 /* 131 * (non-Javadoc) 132 * 133 * @see org.graphstream.stream.file.FileSource#begin(java.net.URL) 134 */ 135 public void begin(URL url) throws IOException { 136 begin(url.openStream()); 137 } 138 139 /* 140 * (non-Javadoc) 141 * 142 * @see org.graphstream.stream.file.FileSource#begin(java.io.InputStream) 143 */ 144 public void begin(InputStream stream) throws IOException { 145 begin(new InputStreamReader(stream)); 146 } 147 148 /* 149 * (non-Javadoc) 150 * 151 * @see org.graphstream.stream.file.FileSource#begin(java.io.Reader) 152 */ 153 public void begin(Reader reader) throws IOException { 154 openStream(reader); 155 } 156 157 /** 158 * Called after the event 159 * {@link javax.xml.stream.events.XMLEvent.START_DOCUMENT} has been 160 * received. 161 * 162 * @throws IOException 163 * @throws XMLStreamException 164 */ 165 protected abstract void afterStartDocument() throws IOException, 166 XMLStreamException; 167 168 /** 169 * Called before trying to receive the events 170 * {@link javax.xml.stream.event.END_DOCUMENT}. 171 * 172 * @throws IOException 173 * @throws XMLStreamException 174 */ 175 protected abstract void beforeEndDocument() throws IOException, 176 XMLStreamException; 177 178 /* 179 * (non-Javadoc) 180 * 181 * @see org.graphstream.stream.file.FileSource#nextEvents() 182 */ 183 public abstract boolean nextEvents() throws IOException; 184 185 /* 186 * (non-Javadoc) 187 * 188 * @see org.graphstream.stream.file.FileSource#nextStep() 189 */ 190 public boolean nextStep() throws IOException { 191 return nextEvents(); 192 } 193 194 /* 195 * (non-Javadoc) 196 * 197 * @see org.graphstream.stream.file.FileSource#end() 198 */ 199 public void end() throws IOException { 200 closeStream(); 201 } 202 203 /** 204 * Get a new event from the stream. This method has to be used to allow the 205 * {@link #pushback(XMLEvent)} method to work. 206 * 207 * @return the next event in the stream 208 * @throws IOException 209 * @throws XMLStreamException 210 */ 211 protected XMLEvent getNextEvent() throws IOException, XMLStreamException { 212 skipWhiteSpaces(); 213 214 if (events.size() > 0) 215 return events.pop(); 216 217 return reader.nextEvent(); 218 } 219 220 /** 221 * Pushback an event in the stream. 222 * 223 * @param e 224 * the event 225 */ 226 protected void pushback(XMLEvent e) { 227 events.push(e); 228 } 229 230 /** 231 * Generate a new parse exception. 232 * 233 * @param e 234 * event producing an error 235 * @param msg 236 * message to put in the exception 237 * @param args 238 * arguments of the message 239 * @return a new parse exception 240 */ 241 protected XMLStreamException newParseError(XMLEvent e, String msg, 242 Object... args) { 243 return new XMLStreamException(String.format(msg, args), e.getLocation()); 244 } 245 246 /** 247 * Check is an event has an expected type and name. 248 * 249 * @param e 250 * event to check 251 * @param type 252 * expected type 253 * @param name 254 * expected name 255 * @return true is type and name are valid 256 */ 257 protected boolean isEvent(XMLEvent e, int type, String name) { 258 boolean valid = e.getEventType() == type; 259 260 if (valid) { 261 switch (type) { 262 case START_ELEMENT: 263 valid = e.asStartElement().getName().getLocalPart() 264 .equals(name); 265 break; 266 case END_ELEMENT: 267 valid = e.asEndElement().getName().getLocalPart().equals(name); 268 break; 269 case ATTRIBUTE: 270 valid = ((Attribute) e).getName().getLocalPart().equals(name); 271 break; 272 case CHARACTERS: 273 case NAMESPACE: 274 case PROCESSING_INSTRUCTION: 275 case COMMENT: 276 case START_DOCUMENT: 277 case END_DOCUMENT: 278 case DTD: 279 } 280 } 281 282 return valid; 283 } 284 285 /** 286 * Check is the event has valid type and name. If not, a new exception is 287 * thrown. 288 * 289 * @param e 290 * event to check 291 * @param type 292 * expected type 293 * @param name 294 * expected name 295 * @throws XMLStreamException 296 * if event has invalid type or name 297 */ 298 protected void checkValid(XMLEvent e, int type, String name) 299 throws XMLStreamException { 300 boolean valid = isEvent(e, type, name); 301 302 if (!valid) 303 throw newParseError(e, "expecting %s, got %s", gotWhat(type, name), 304 gotWhat(e)); 305 } 306 307 private String gotWhat(XMLEvent e) { 308 String v = null; 309 310 switch (e.getEventType()) { 311 case START_ELEMENT: 312 v = e.asStartElement().getName().getLocalPart(); 313 break; 314 case END_ELEMENT: 315 v = e.asEndElement().getName().getLocalPart(); 316 break; 317 case ATTRIBUTE: 318 v = ((Attribute) e).getName().getLocalPart(); 319 break; 320 } 321 322 return gotWhat(e.getEventType(), v); 323 } 324 325 private String gotWhat(int type, String v) { 326 switch (type) { 327 case START_ELEMENT: 328 return String.format("'<%s>'", v); 329 case END_ELEMENT: 330 return String.format("'</%s>'", v); 331 case ATTRIBUTE: 332 return String.format("attribute '%s'", v); 333 case NAMESPACE: 334 return "namespace"; 335 case PROCESSING_INSTRUCTION: 336 return "processing instruction"; 337 case COMMENT: 338 return "comment"; 339 case START_DOCUMENT: 340 return "document start"; 341 case END_DOCUMENT: 342 return "document end"; 343 case DTD: 344 return "dtd"; 345 case CHARACTERS: 346 return "characters"; 347 default: 348 return "UNKNOWN"; 349 } 350 } 351 352 private void skipWhiteSpaces() throws IOException, XMLStreamException { 353 XMLEvent e; 354 355 do { 356 if (events.size() > 0) 357 e = events.pop(); 358 else 359 e = reader.nextEvent(); 360 } while (isEvent(e, XMLEvent.CHARACTERS, null) 361 && e.asCharacters().getData().matches("^\\s*$")); 362 363 pushback(e); 364 } 365 366 /** 367 * Open a new xml events stream. 368 * 369 * @param stream 370 * @throws IOException 371 */ 372 protected void openStream(Reader stream) throws IOException { 373 if (reader != null) 374 closeStream(); 375 376 try { 377 XMLEvent e; 378 379 reader = XMLInputFactory.newInstance().createXMLEventReader(stream); 380 381 e = getNextEvent(); 382 checkValid(e, XMLEvent.START_DOCUMENT, null); 383 384 afterStartDocument(); 385 } catch (XMLStreamException e) { 386 throw new IOException(e); 387 } catch (FactoryConfigurationError e) { 388 throw new IOException(e); 389 } 390 } 391 392 /** 393 * Close the current opened stream. 394 * 395 * @throws IOException 396 */ 397 protected void closeStream() throws IOException { 398 try { 399 beforeEndDocument(); 400 reader.close(); 401 } catch (XMLStreamException e) { 402 throw new IOException(e); 403 } finally { 404 reader = null; 405 } 406 } 407 408 /** 409 * Convert an attribute to a valid constant name. 410 * 411 * @see #toConstantName(String) 412 * @param a 413 * @return 414 */ 415 protected String toConstantName(Attribute a) { 416 return toConstantName(a.getName().getLocalPart()); 417 } 418 419 /** 420 * Convert a string to a valid constant name. String is put to upper case 421 * and all non-word characters are replaced by '_'. 422 * 423 * @param value 424 * @return 425 */ 426 protected String toConstantName(String value) { 427 return value.toUpperCase().replaceAll("\\W", "_"); 428 } 429 430 /** 431 * Base for parsers, providing some usefull features. 432 * 433 */ 434 protected class Parser { 435 /** 436 * Read a sequence of characters and return these characters as a 437 * string. Characters are read until a non-character event is reached. 438 * 439 * @return a sequence of characters 440 * @throws IOException 441 * @throws XMLStreamException 442 */ 443 protected String __characters() throws IOException, XMLStreamException { 444 XMLEvent e; 445 StringBuilder buffer = new StringBuilder(); 446 447 e = getNextEvent(); 448 449 while (e.getEventType() == XMLEvent.CHARACTERS) { 450 buffer.append(e.asCharacters()); 451 e = getNextEvent(); 452 } 453 454 pushback(e); 455 456 return buffer.toString(); 457 } 458 459 /** 460 * Get attributes of a start element in a map. Attributes should be 461 * described in an enumeration such that 462 * {@link FileSourceXML#toConstantName(Attribute)} correspond to names 463 * of enumeration constants. 464 * 465 * @param <T> 466 * type of the enumeration describing attributes 467 * @param cls 468 * class of the enumeration T 469 * @param e 470 * start event from which attributes have to be extracted 471 * @return a mapping between enum constants and attribute values. 472 */ 473 protected <T extends Enum<T>> EnumMap<T, String> getAttributes( 474 Class<T> cls, StartElement e) { 475 EnumMap<T, String> values = new EnumMap<T, String>(cls); 476 477 @SuppressWarnings("unchecked") 478 Iterator<? extends Attribute> attributes = e.asStartElement() 479 .getAttributes(); 480 481 while (attributes.hasNext()) { 482 Attribute a = attributes.next(); 483 484 for (int i = 0; i < cls.getEnumConstants().length; i++) { 485 if (cls.getEnumConstants()[i].name().equals( 486 toConstantName(a))) { 487 values.put(cls.getEnumConstants()[i], a.getValue()); 488 break; 489 } 490 } 491 } 492 493 return values; 494 } 495 496 /** 497 * Check if all required attributes are present. 498 * 499 * @param <T> 500 * type of the enumeration describing attributes 501 * @param e 502 * the event 503 * @param attributes 504 * extracted attributes 505 * @param required 506 * array of required attributes 507 * @throws XMLStreamException 508 * if at least one required attribute is not found 509 */ 510 protected <T extends Enum<T>> void checkRequiredAttributes(XMLEvent e, 511 EnumMap<T, String> attributes, T... required) 512 throws XMLStreamException { 513 if (required != null) { 514 for (int i = 0; i < required.length; i++) { 515 if (!attributes.containsKey(required[i])) 516 throw newParseError(e, 517 "'%s' attribute is required for <%s> element", 518 required[i].name().toLowerCase(), e 519 .asStartElement().getName() 520 .getLocalPart()); 521 } 522 } 523 } 524 } 525}