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}