src/net/fortuna/ical4j/data/CalendarParserImpl.java

Tue, 10 Feb 2015 19:58:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 10 Feb 2015 19:58:00 +0100
changeset 4
45d57ecba757
parent 0
fb9019fb1bf7
permissions
-rw-r--r--

Upgrade the upgraded ical4j component to use org.apache.commons.lang3.

michael@0 1 /**
michael@0 2 * Copyright (c) 2012, Ben Fortuna
michael@0 3 * All rights reserved.
michael@0 4 *
michael@0 5 * Redistribution and use in source and binary forms, with or without
michael@0 6 * modification, are permitted provided that the following conditions
michael@0 7 * are met:
michael@0 8 *
michael@0 9 * o Redistributions of source code must retain the above copyright
michael@0 10 * notice, this list of conditions and the following disclaimer.
michael@0 11 *
michael@0 12 * o Redistributions in binary form must reproduce the above copyright
michael@0 13 * notice, this list of conditions and the following disclaimer in the
michael@0 14 * documentation and/or other materials provided with the distribution.
michael@0 15 *
michael@0 16 * o Neither the name of Ben Fortuna nor the names of any other contributors
michael@0 17 * may be used to endorse or promote products derived from this software
michael@0 18 * without specific prior written permission.
michael@0 19 *
michael@0 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
michael@0 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
michael@0 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
michael@0 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
michael@0 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
michael@0 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
michael@0 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
michael@0 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
michael@0 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
michael@0 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
michael@0 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
michael@0 31 */
michael@0 32 package net.fortuna.ical4j.data;
michael@0 33
michael@0 34 import java.io.IOException;
michael@0 35 import java.io.InputStream;
michael@0 36 import java.io.InputStreamReader;
michael@0 37 import java.io.Reader;
michael@0 38 import java.io.StreamTokenizer;
michael@0 39 import java.net.URISyntaxException;
michael@0 40 import java.text.MessageFormat;
michael@0 41 import java.text.ParseException;
michael@0 42
michael@0 43 import net.fortuna.ical4j.model.Calendar;
michael@0 44 import net.fortuna.ical4j.model.Component;
michael@0 45
michael@0 46 import org.apache.commons.logging.Log;
michael@0 47 import org.apache.commons.logging.LogFactory;
michael@0 48
michael@0 49 /**
michael@0 50 * <pre>
michael@0 51 * $Id$
michael@0 52 *
michael@0 53 * Created [Nov 5, 2004]
michael@0 54 * </pre>
michael@0 55 *
michael@0 56 * The default implementation of a calendar parser.
michael@0 57 * @author Ben Fortuna
michael@0 58 */
michael@0 59 public class CalendarParserImpl implements CalendarParser {
michael@0 60
michael@0 61 private static final int WORD_CHAR_START = 32;
michael@0 62
michael@0 63 private static final int WORD_CHAR_END = 255;
michael@0 64
michael@0 65 private static final int WHITESPACE_CHAR_START = 0;
michael@0 66
michael@0 67 private static final int WHITESPACE_CHAR_END = 20;
michael@0 68
michael@0 69 private static final String UNEXPECTED_TOKEN_MESSAGE = "Expected [{0}], read [{1}]";
michael@0 70
michael@0 71 private Log log = LogFactory.getLog(CalendarParserImpl.class);
michael@0 72
michael@0 73 private final ComponentListParser componentListParser = new ComponentListParser();
michael@0 74
michael@0 75 private final ComponentParser componentParser = new ComponentParser();
michael@0 76
michael@0 77 private final PropertyListParser propertyListParser = new PropertyListParser();
michael@0 78
michael@0 79 private final PropertyParser propertyParser = new PropertyParser();
michael@0 80
michael@0 81 private final ParameterListParser paramListParser = new ParameterListParser();
michael@0 82
michael@0 83 private final ParameterParser paramParser = new ParameterParser();
michael@0 84
michael@0 85 /**
michael@0 86 * {@inheritDoc}
michael@0 87 */
michael@0 88 public final void parse(final InputStream in, final ContentHandler handler)
michael@0 89 throws IOException, ParserException {
michael@0 90 parse(new InputStreamReader(in), handler);
michael@0 91 }
michael@0 92
michael@0 93 /**
michael@0 94 * {@inheritDoc}
michael@0 95 */
michael@0 96 public final void parse(final Reader in, final ContentHandler handler)
michael@0 97 throws IOException, ParserException {
michael@0 98
michael@0 99 final StreamTokenizer tokeniser = new StreamTokenizer(in);
michael@0 100 try {
michael@0 101 tokeniser.resetSyntax();
michael@0 102 tokeniser.wordChars(WORD_CHAR_START, WORD_CHAR_END);
michael@0 103 tokeniser.whitespaceChars(WHITESPACE_CHAR_START,
michael@0 104 WHITESPACE_CHAR_END);
michael@0 105 tokeniser.ordinaryChar(':');
michael@0 106 tokeniser.ordinaryChar(';');
michael@0 107 tokeniser.ordinaryChar('=');
michael@0 108 tokeniser.ordinaryChar('\t');
michael@0 109 tokeniser.eolIsSignificant(true);
michael@0 110 tokeniser.whitespaceChars(0, 0);
michael@0 111 tokeniser.quoteChar('"');
michael@0 112
michael@0 113 // BEGIN:VCALENDAR
michael@0 114 assertToken(tokeniser, in, Calendar.BEGIN);
michael@0 115
michael@0 116 assertToken(tokeniser, in, ':');
michael@0 117
michael@0 118 assertToken(tokeniser, in, Calendar.VCALENDAR, true);
michael@0 119
michael@0 120 assertToken(tokeniser, in, StreamTokenizer.TT_EOL);
michael@0 121
michael@0 122 handler.startCalendar();
michael@0 123
michael@0 124 // parse calendar properties..
michael@0 125 propertyListParser.parse(tokeniser, in, handler);
michael@0 126
michael@0 127 // parse components..
michael@0 128 componentListParser.parse(tokeniser, in, handler);
michael@0 129
michael@0 130 // END:VCALENDAR
michael@0 131 // assertToken(tokeniser,Calendar.END);
michael@0 132
michael@0 133 assertToken(tokeniser, in, ':');
michael@0 134
michael@0 135 assertToken(tokeniser, in, Calendar.VCALENDAR, true);
michael@0 136
michael@0 137 handler.endCalendar();
michael@0 138 }
michael@0 139 catch (Exception e) {
michael@0 140
michael@0 141 if (e instanceof IOException) {
michael@0 142 throw (IOException) e;
michael@0 143 }
michael@0 144 if (e instanceof ParserException) {
michael@0 145 throw (ParserException) e;
michael@0 146 }
michael@0 147 else {
michael@0 148 throw new ParserException(e.getMessage(), getLineNumber(tokeniser, in), e);
michael@0 149 }
michael@0 150 }
michael@0 151 }
michael@0 152
michael@0 153 /**
michael@0 154 * Parses an iCalendar property list from the specified stream tokeniser.
michael@0 155 * @param tokeniser
michael@0 156 * @throws IOException
michael@0 157 * @throws ParseException
michael@0 158 * @throws URISyntaxException
michael@0 159 * @throws URISyntaxException
michael@0 160 * @throws ParserException
michael@0 161 */
michael@0 162 private class PropertyListParser {
michael@0 163
michael@0 164 public void parse(final StreamTokenizer tokeniser, Reader in,
michael@0 165 final ContentHandler handler) throws IOException, ParseException,
michael@0 166 URISyntaxException, ParserException {
michael@0 167
michael@0 168 assertToken(tokeniser, in, StreamTokenizer.TT_WORD);
michael@0 169
michael@0 170 while (/*
michael@0 171 * !Component.BEGIN.equals(tokeniser.sval) &&
michael@0 172 */!Component.END.equals(tokeniser.sval)) {
michael@0 173 // check for timezones observances or vevent/vtodo alarms..
michael@0 174 if (Component.BEGIN.equals(tokeniser.sval)) {
michael@0 175 componentParser.parse(tokeniser, in, handler);
michael@0 176 }
michael@0 177 else {
michael@0 178 propertyParser.parse(tokeniser, in, handler);
michael@0 179 }
michael@3 180 absorbWhitespace(tokeniser, in);
michael@0 181 // assertToken(tokeniser, StreamTokenizer.TT_WORD);
michael@0 182 }
michael@0 183 }
michael@0 184 }
michael@0 185
michael@0 186 /**
michael@0 187 * Parses an iCalendar property from the specified stream tokeniser.
michael@0 188 * @param tokeniser
michael@0 189 * @throws IOException
michael@0 190 * @throws ParserException
michael@0 191 * @throws URISyntaxException
michael@0 192 * @throws ParseException
michael@0 193 */
michael@0 194 private class PropertyParser {
michael@0 195
michael@0 196 private static final String PARSE_DEBUG_MESSAGE = "Property [{0}]";
michael@0 197
michael@0 198 private static final String PARSE_EXCEPTION_MESSAGE = "Property [{0}]";
michael@0 199
michael@0 200 private void parse(final StreamTokenizer tokeniser, Reader in,
michael@0 201 final ContentHandler handler) throws IOException, ParserException,
michael@0 202 URISyntaxException, ParseException {
michael@0 203
michael@0 204 final String name = tokeniser.sval;
michael@0 205
michael@0 206 // debugging..
michael@0 207 if (log.isDebugEnabled()) {
michael@0 208 log.debug(MessageFormat.format(PARSE_DEBUG_MESSAGE, new Object[] {name}));
michael@0 209 }
michael@0 210
michael@0 211 handler.startProperty(name);
michael@0 212
michael@0 213 paramListParser.parse(tokeniser, in, handler);
michael@0 214
michael@0 215 // it appears that control tokens (ie. ':') are allowed
michael@0 216 // after the first instance on a line is used.. as such
michael@0 217 // we must continue appending to value until EOL is
michael@0 218 // reached..
michael@0 219 // assertToken(tokeniser, StreamTokenizer.TT_WORD);
michael@0 220
michael@0 221 // String value = tokeniser.sval;
michael@0 222 final StringBuffer value = new StringBuffer();
michael@0 223
michael@0 224 // assertToken(tokeniser,StreamTokenizer.TT_EOL);
michael@0 225
michael@0 226 // DQUOTE is ordinary char for property value
michael@0 227 // From sec 4.3.11 of rfc-2445:
michael@0 228 // text = *(TSAFE-CHAR / ":" / DQUOTE / ESCAPED-CHAR)
michael@0 229 //
michael@0 230 tokeniser.ordinaryChar('"');
michael@3 231 int nextToken = nextToken(tokeniser, in);
michael@0 232
michael@3 233 while (nextToken != StreamTokenizer.TT_EOL) {
michael@0 234
michael@0 235 if (tokeniser.ttype == StreamTokenizer.TT_WORD) {
michael@0 236 value.append(tokeniser.sval);
michael@0 237 }
michael@0 238 else {
michael@0 239 value.append((char) tokeniser.ttype);
michael@0 240 }
michael@0 241
michael@3 242 nextToken = nextToken(tokeniser, in);
michael@0 243 }
michael@0 244
michael@0 245 // reset DQUOTE to be quote char
michael@0 246 tokeniser.quoteChar('"');
michael@0 247
michael@0 248 try {
michael@0 249 handler.propertyValue(value.toString());
michael@0 250 }
michael@0 251 catch (ParseException e) {
michael@0 252 final ParseException eNew = new ParseException("[" + name + "] "
michael@0 253 + e.getMessage(), e.getErrorOffset());
michael@0 254 eNew.initCause(e);
michael@0 255 throw eNew;
michael@0 256 }
michael@0 257
michael@0 258 handler.endProperty(name);
michael@0 259
michael@0 260 }
michael@0 261 }
michael@0 262
michael@0 263 /**
michael@0 264 * Parses a list of iCalendar parameters by parsing the specified stream tokeniser.
michael@0 265 * @param tokeniser
michael@0 266 * @throws IOException
michael@0 267 * @throws ParserException
michael@0 268 * @throws URISyntaxException
michael@0 269 */
michael@0 270 private class ParameterListParser {
michael@0 271
michael@0 272 public void parse(final StreamTokenizer tokeniser, Reader in,
michael@0 273 final ContentHandler handler) throws IOException, ParserException,
michael@0 274 URISyntaxException {
michael@0 275
michael@3 276 while (nextToken(tokeniser, in) == ';') {
michael@0 277 paramParser.parse(tokeniser, in, handler);
michael@0 278 }
michael@0 279 }
michael@0 280 }
michael@0 281
michael@0 282 /**
michael@0 283 * @param tokeniser
michael@0 284 * @param handler
michael@0 285 * @throws IOException
michael@0 286 * @throws ParserException
michael@0 287 * @throws URISyntaxException
michael@0 288 */
michael@0 289 private class ParameterParser {
michael@0 290
michael@0 291 private void parse(final StreamTokenizer tokeniser, Reader in,
michael@0 292 final ContentHandler handler) throws IOException, ParserException,
michael@0 293 URISyntaxException {
michael@0 294
michael@0 295 assertToken(tokeniser, in, StreamTokenizer.TT_WORD);
michael@0 296
michael@0 297 final String paramName = tokeniser.sval;
michael@0 298
michael@0 299 // debugging..
michael@0 300 if (log.isDebugEnabled()) {
michael@0 301 log.debug("Parameter [" + paramName + "]");
michael@0 302 }
michael@0 303
michael@0 304 assertToken(tokeniser, in, '=');
michael@0 305
michael@0 306 final StringBuffer paramValue = new StringBuffer();
michael@0 307
michael@0 308 // preserve quote chars..
michael@3 309 if (nextToken(tokeniser, in) == '"') {
michael@0 310 paramValue.append('"');
michael@0 311 paramValue.append(tokeniser.sval);
michael@0 312 paramValue.append('"');
michael@0 313 }
michael@0 314 else if (tokeniser.sval != null) {
michael@0 315 paramValue.append(tokeniser.sval);
michael@0 316 // check for additional words to account for equals (=) in param-value
michael@3 317 int nextToken = nextToken(tokeniser, in);
michael@0 318
michael@0 319 while (nextToken != ';' && nextToken != ':' && nextToken != ',') {
michael@0 320
michael@0 321 if (tokeniser.ttype == StreamTokenizer.TT_WORD) {
michael@0 322 paramValue.append(tokeniser.sval);
michael@0 323 }
michael@0 324 else {
michael@0 325 paramValue.append((char) tokeniser.ttype);
michael@0 326 }
michael@0 327
michael@3 328 nextToken = nextToken(tokeniser, in);
michael@0 329 }
michael@0 330 tokeniser.pushBack();
michael@0 331 } else if(tokeniser.sval == null) {
michael@0 332 tokeniser.pushBack();
michael@0 333 }
michael@0 334
michael@0 335 try {
michael@0 336 handler.parameter(paramName, paramValue.toString());
michael@0 337 }
michael@0 338 catch (ClassCastException cce) {
michael@0 339 throw new ParserException("Error parsing parameter", getLineNumber(tokeniser, in), cce);
michael@0 340 }
michael@0 341 }
michael@0 342 }
michael@0 343
michael@0 344 /**
michael@0 345 * Parses an iCalendar component list from the specified stream tokeniser.
michael@0 346 * @param tokeniser
michael@0 347 * @throws IOException
michael@0 348 * @throws ParseException
michael@0 349 * @throws URISyntaxException
michael@0 350 * @throws ParserException
michael@0 351 */
michael@0 352 private class ComponentListParser {
michael@0 353
michael@0 354 private void parse(final StreamTokenizer tokeniser, Reader in,
michael@0 355 final ContentHandler handler) throws IOException, ParseException,
michael@0 356 URISyntaxException, ParserException {
michael@0 357
michael@0 358 while (Component.BEGIN.equals(tokeniser.sval)) {
michael@0 359 componentParser.parse(tokeniser, in, handler);
michael@3 360 absorbWhitespace(tokeniser, in);
michael@0 361 // assertToken(tokeniser, StreamTokenizer.TT_WORD);
michael@0 362 }
michael@0 363 }
michael@0 364 }
michael@0 365
michael@0 366 /**
michael@0 367 * Parses an iCalendar component from the specified stream tokeniser.
michael@0 368 * @param tokeniser
michael@0 369 * @throws IOException
michael@0 370 * @throws ParseException
michael@0 371 * @throws URISyntaxException
michael@0 372 * @throws ParserException
michael@0 373 */
michael@0 374 private class ComponentParser {
michael@0 375
michael@0 376 private void parse(final StreamTokenizer tokeniser, Reader in,
michael@0 377 final ContentHandler handler) throws IOException, ParseException,
michael@0 378 URISyntaxException, ParserException {
michael@0 379
michael@0 380 assertToken(tokeniser, in, ':');
michael@0 381
michael@0 382 assertToken(tokeniser, in, StreamTokenizer.TT_WORD);
michael@0 383
michael@0 384 final String name = tokeniser.sval;
michael@0 385
michael@0 386 handler.startComponent(name);
michael@0 387
michael@0 388 assertToken(tokeniser, in, StreamTokenizer.TT_EOL);
michael@0 389
michael@0 390 propertyListParser.parse(tokeniser, in, handler);
michael@0 391
michael@0 392 /*
michael@0 393 * // a special case for VTIMEZONE component which contains
michael@0 394 * // sub-components..
michael@0 395 * if (Component.VTIMEZONE.equals(name)) {
michael@0 396 * parseComponentList(tokeniser, handler);
michael@0 397 * }
michael@0 398 * // VEVENT/VTODO components may optionally have embedded VALARM
michael@0 399 * // components..
michael@0 400 * else if ((Component.VEVENT.equals(name) || Component.VTODO.equals(name))
michael@0 401 * &amp;&amp; Component.BEGIN.equals(tokeniser.sval)) {
michael@0 402 * parseComponentList(tokeniser, handler);
michael@0 403 * }
michael@0 404 */
michael@0 405
michael@0 406 assertToken(tokeniser, in, ':');
michael@0 407
michael@0 408 assertToken(tokeniser, in, name);
michael@0 409
michael@0 410 assertToken(tokeniser, in, StreamTokenizer.TT_EOL);
michael@0 411
michael@0 412 handler.endComponent(name);
michael@0 413 }
michael@0 414 }
michael@0 415
michael@0 416 /**
michael@0 417 * Asserts that the next token in the stream matches the specified token.
michael@0 418 * @param tokeniser stream tokeniser to perform assertion on
michael@0 419 * @param token expected token
michael@0 420 * @throws IOException when unable to read from stream
michael@0 421 * @throws ParserException when next token in the stream does not match the expected token
michael@0 422 */
michael@0 423 private void assertToken(final StreamTokenizer tokeniser, Reader in, final int token)
michael@0 424 throws IOException, ParserException {
michael@0 425
michael@3 426 if (nextToken(tokeniser, in) != token) {
michael@0 427 throw new ParserException(MessageFormat.format(UNEXPECTED_TOKEN_MESSAGE, new Object[] {
michael@0 428 new Integer(token), new Integer(tokeniser.ttype),
michael@0 429 }), getLineNumber(tokeniser, in));
michael@0 430 }
michael@0 431
michael@0 432 if (log.isDebugEnabled()) {
michael@0 433 log.debug("[" + token + "]");
michael@0 434 }
michael@0 435 }
michael@0 436
michael@0 437 /**
michael@0 438 * Asserts that the next token in the stream matches the specified token. This method is case-sensitive.
michael@0 439 * @param tokeniser
michael@0 440 * @param token
michael@0 441 * @throws IOException
michael@0 442 * @throws ParserException
michael@0 443 */
michael@0 444 private void assertToken(final StreamTokenizer tokeniser, Reader in, final String token)
michael@0 445 throws IOException, ParserException {
michael@0 446 assertToken(tokeniser, in, token, false);
michael@0 447 }
michael@0 448
michael@0 449 /**
michael@0 450 * Asserts that the next token in the stream matches the specified token.
michael@0 451 * @param tokeniser stream tokeniser to perform assertion on
michael@0 452 * @param token expected token
michael@0 453 * @throws IOException when unable to read from stream
michael@0 454 * @throws ParserException when next token in the stream does not match the expected token
michael@0 455 */
michael@0 456 private void assertToken(final StreamTokenizer tokeniser, Reader in,
michael@0 457 final String token, final boolean ignoreCase) throws IOException,
michael@0 458 ParserException {
michael@0 459
michael@0 460 // ensure next token is a word token..
michael@0 461 assertToken(tokeniser, in, StreamTokenizer.TT_WORD);
michael@0 462
michael@0 463 if (ignoreCase) {
michael@0 464 if (!token.equalsIgnoreCase(tokeniser.sval)) {
michael@0 465 throw new ParserException(MessageFormat.format(UNEXPECTED_TOKEN_MESSAGE, new Object[] {
michael@0 466 token, tokeniser.sval,
michael@0 467 }), getLineNumber(tokeniser, in));
michael@0 468 }
michael@0 469 }
michael@0 470 else if (!token.equals(tokeniser.sval)) {
michael@0 471 throw new ParserException(MessageFormat.format(UNEXPECTED_TOKEN_MESSAGE, new Object[] {
michael@0 472 token, tokeniser.sval,
michael@0 473 }), getLineNumber(tokeniser, in));
michael@0 474 }
michael@0 475
michael@0 476 if (log.isDebugEnabled()) {
michael@0 477 log.debug("[" + token + "]");
michael@0 478 }
michael@0 479 }
michael@0 480
michael@0 481 /**
michael@0 482 * Absorbs extraneous newlines.
michael@0 483 * @param tokeniser
michael@0 484 * @throws IOException
michael@0 485 */
michael@3 486 private void absorbWhitespace(final StreamTokenizer tokeniser, Reader in) throws IOException, ParserException {
michael@0 487 // HACK: absorb extraneous whitespace between components (KOrganizer)..
michael@3 488 while (nextToken(tokeniser, in) == StreamTokenizer.TT_EOL) {
michael@0 489 if (log.isTraceEnabled()) {
michael@0 490 log.trace("Absorbing extra whitespace..");
michael@0 491 }
michael@0 492 }
michael@0 493 if (log.isTraceEnabled()) {
michael@0 494 log.trace("Aborting: absorbing extra whitespace complete");
michael@0 495 }
michael@0 496 }
michael@0 497
michael@0 498 /**
michael@0 499 * @param tokeniser
michael@0 500 * @param in
michael@0 501 * @return
michael@0 502 */
michael@0 503 private int getLineNumber(StreamTokenizer tokeniser, Reader in) {
michael@0 504 int line = tokeniser.lineno();
michael@0 505 if (tokeniser.ttype == StreamTokenizer.TT_EOL) {
michael@0 506 line -= 1;
michael@0 507 }
michael@0 508 if (in instanceof UnfoldingReader) {
michael@0 509 // need to take unfolded lines into account
michael@0 510 final int unfolded = ((UnfoldingReader) in).getLinesUnfolded();
michael@0 511 line += unfolded;
michael@0 512 }
michael@0 513 return line;
michael@0 514 }
michael@3 515
michael@3 516 /**
michael@3 517 * Reads the next token from the tokeniser.
michael@3 518 * This method throws a ParseException when reading EOF.
michael@3 519 * @param tokeniser
michael@3 520 * @param in
michael@3 521 * @return
michael@3 522 * @throws ParseException When reading EOF.
michael@3 523 */
michael@3 524 private int nextToken(StreamTokenizer tokeniser, Reader in) throws IOException, ParserException {
michael@3 525 int token = tokeniser.nextToken();
michael@3 526 if (token == StreamTokenizer.TT_EOF) {
michael@3 527 throw new ParserException("Unexpected end of file", getLineNumber(tokeniser, in));
michael@3 528 }
michael@3 529 return token;
michael@3 530 }
michael@0 531 }

mercurial