1/*
2 * ProGuard -- shrinking, optimization, obfuscation, and preverification
3 *             of Java bytecode.
4 *
5 * Copyright (c) 2002-2014 Eric Lafortune (eric@graphics.cornell.edu)
6 *
7 * This program is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the Free
9 * Software Foundation; either version 2 of the License, or (at your option)
10 * any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 * more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 */
21package proguard;
22
23import proguard.classfile.*;
24import proguard.classfile.util.ClassUtil;
25import proguard.util.ListUtil;
26
27import java.io.*;
28import java.net.URL;
29import java.util.*;
30
31
32/**
33 * This class parses ProGuard configurations. Configurations can be read from an
34 * array of arguments or from a configuration file or URL. External references
35 * in file names ('<...>') can be resolved against a given set of properties.
36 *
37 * @author Eric Lafortune
38 */
39public class ConfigurationParser
40{
41    private final WordReader reader;
42    private final Properties properties;
43
44    private String     nextWord;
45    private String     lastComments;
46
47
48    /**
49     * Creates a new ConfigurationParser for the given String arguments and
50     * the given Properties.
51     */
52    public ConfigurationParser(String[]   args,
53                               Properties properties) throws IOException
54    {
55        this(args, null, properties);
56    }
57
58
59    /**
60     * Creates a new ConfigurationParser for the given String arguments,
61     * with the given base directory and the given Properties.
62     */
63    public ConfigurationParser(String[]   args,
64                               File       baseDir,
65                               Properties properties) throws IOException
66    {
67        this(new ArgumentWordReader(args, baseDir), properties);
68    }
69
70
71    /**
72     * Creates a new ConfigurationParser for the given lines,
73     * with the given base directory and the given Properties.
74     */
75    public ConfigurationParser(String     lines,
76                               String     description,
77                               File       baseDir,
78                               Properties properties) throws IOException
79    {
80        this(new LineWordReader(new LineNumberReader(new StringReader(lines)),
81                                description,
82                                baseDir),
83             properties);
84    }
85
86
87    /**
88     * Creates a new ConfigurationParser for the given file, with the system
89     * Properties.
90     * @deprecated Temporary code for backward compatibility in Obclipse.
91     */
92    public ConfigurationParser(File file) throws IOException
93    {
94        this(file, System.getProperties());
95    }
96
97
98    /**
99     * Creates a new ConfigurationParser for the given file and the given
100     * Properties.
101     */
102    public ConfigurationParser(File       file,
103                               Properties properties) throws IOException
104    {
105        this(new FileWordReader(file), properties);
106    }
107
108
109    /**
110     * Creates a new ConfigurationParser for the given URL and the given
111     * Properties.
112     */
113    public ConfigurationParser(URL        url,
114                               Properties properties) throws IOException
115    {
116        this(new FileWordReader(url), properties);
117    }
118
119
120    /**
121     * Creates a new ConfigurationParser for the given word reader and the
122     * given Properties.
123     */
124    public ConfigurationParser(WordReader reader,
125                               Properties properties) throws IOException
126    {
127        this.reader     = reader;
128        this.properties = properties;
129
130        readNextWord();
131    }
132
133
134    /**
135     * Parses and returns the configuration.
136     * @param configuration the configuration that is updated as a side-effect.
137     * @throws ParseException if the any of the configuration settings contains
138     *                        a syntax error.
139     * @throws IOException if an IO error occurs while reading a configuration.
140     */
141    public void parse(Configuration configuration)
142    throws ParseException, IOException
143    {
144        while (nextWord != null)
145        {
146            lastComments = reader.lastComments();
147
148            // First include directives.
149            if      (ConfigurationConstants.AT_DIRECTIVE                                     .startsWith(nextWord) ||
150                     ConfigurationConstants.INCLUDE_DIRECTIVE                                .startsWith(nextWord)) configuration.lastModified                     = parseIncludeArgument(configuration.lastModified);
151            else if (ConfigurationConstants.BASE_DIRECTORY_DIRECTIVE                         .startsWith(nextWord)) parseBaseDirectoryArgument();
152
153            // Then configuration options with or without arguments.
154            else if (ConfigurationConstants.INJARS_OPTION                                    .startsWith(nextWord)) configuration.programJars                      = parseClassPathArgument(configuration.programJars, false);
155            else if (ConfigurationConstants.OUTJARS_OPTION                                   .startsWith(nextWord)) configuration.programJars                      = parseClassPathArgument(configuration.programJars, true);
156            else if (ConfigurationConstants.LIBRARYJARS_OPTION                               .startsWith(nextWord)) configuration.libraryJars                      = parseClassPathArgument(configuration.libraryJars, false);
157            else if (ConfigurationConstants.RESOURCEJARS_OPTION                              .startsWith(nextWord)) throw new ParseException("The '-resourcejars' option is no longer supported. Please use the '-injars' option for all input");
158            else if (ConfigurationConstants.SKIP_NON_PUBLIC_LIBRARY_CLASSES_OPTION           .startsWith(nextWord)) configuration.skipNonPublicLibraryClasses      = parseNoArgument(true);
159            else if (ConfigurationConstants.DONT_SKIP_NON_PUBLIC_LIBRARY_CLASSES_OPTION      .startsWith(nextWord)) configuration.skipNonPublicLibraryClasses      = parseNoArgument(false);
160            else if (ConfigurationConstants.DONT_SKIP_NON_PUBLIC_LIBRARY_CLASS_MEMBERS_OPTION.startsWith(nextWord)) configuration.skipNonPublicLibraryClassMembers = parseNoArgument(false);
161            else if (ConfigurationConstants.TARGET_OPTION                                    .startsWith(nextWord)) configuration.targetClassVersion               = parseClassVersion();
162            else if (ConfigurationConstants.FORCE_PROCESSING_OPTION                          .startsWith(nextWord)) configuration.lastModified                     = parseNoArgument(Long.MAX_VALUE);
163
164            else if (ConfigurationConstants.KEEP_OPTION                                      .startsWith(nextWord)) configuration.keep                             = parseKeepClassSpecificationArguments(configuration.keep, true,  false, false);
165            else if (ConfigurationConstants.KEEP_CLASS_MEMBERS_OPTION                        .startsWith(nextWord)) configuration.keep                             = parseKeepClassSpecificationArguments(configuration.keep, false, false, false);
166            else if (ConfigurationConstants.KEEP_CLASSES_WITH_MEMBERS_OPTION                 .startsWith(nextWord)) configuration.keep                             = parseKeepClassSpecificationArguments(configuration.keep, false, true,  false);
167            else if (ConfigurationConstants.KEEP_NAMES_OPTION                                .startsWith(nextWord)) configuration.keep                             = parseKeepClassSpecificationArguments(configuration.keep, true,  false, true);
168            else if (ConfigurationConstants.KEEP_CLASS_MEMBER_NAMES_OPTION                   .startsWith(nextWord)) configuration.keep                             = parseKeepClassSpecificationArguments(configuration.keep, false, false, true);
169            else if (ConfigurationConstants.KEEP_CLASSES_WITH_MEMBER_NAMES_OPTION            .startsWith(nextWord)) configuration.keep                             = parseKeepClassSpecificationArguments(configuration.keep, false, true,  true);
170            else if (ConfigurationConstants.PRINT_SEEDS_OPTION                               .startsWith(nextWord)) configuration.printSeeds                       = parseOptionalFile();
171
172            // After '-keep'.
173            else if (ConfigurationConstants.KEEP_DIRECTORIES_OPTION                          .startsWith(nextWord)) configuration.keepDirectories                  = parseCommaSeparatedList("directory name", true, true, false, true, false, true, false, false, configuration.keepDirectories);
174
175            else if (ConfigurationConstants.DONT_SHRINK_OPTION                               .startsWith(nextWord)) configuration.shrink                           = parseNoArgument(false);
176            else if (ConfigurationConstants.PRINT_USAGE_OPTION                               .startsWith(nextWord)) configuration.printUsage                       = parseOptionalFile();
177            else if (ConfigurationConstants.WHY_ARE_YOU_KEEPING_OPTION                       .startsWith(nextWord)) configuration.whyAreYouKeeping                 = parseClassSpecificationArguments(configuration.whyAreYouKeeping);
178
179            else if (ConfigurationConstants.DONT_OPTIMIZE_OPTION                             .startsWith(nextWord)) configuration.optimize                         = parseNoArgument(false);
180            else if (ConfigurationConstants.OPTIMIZATION_PASSES                              .startsWith(nextWord)) configuration.optimizationPasses               = parseIntegerArgument();
181            else if (ConfigurationConstants.OPTIMIZATIONS                                    .startsWith(nextWord)) configuration.optimizations                    = parseCommaSeparatedList("optimization name", true, false, false, false, false, false, false, false, configuration.optimizations);
182            else if (ConfigurationConstants.ASSUME_NO_SIDE_EFFECTS_OPTION                    .startsWith(nextWord)) configuration.assumeNoSideEffects              = parseClassSpecificationArguments(configuration.assumeNoSideEffects);
183            else if (ConfigurationConstants.ALLOW_ACCESS_MODIFICATION_OPTION                 .startsWith(nextWord)) configuration.allowAccessModification          = parseNoArgument(true);
184            else if (ConfigurationConstants.MERGE_INTERFACES_AGGRESSIVELY_OPTION             .startsWith(nextWord)) configuration.mergeInterfacesAggressively      = parseNoArgument(true);
185
186            else if (ConfigurationConstants.DONT_OBFUSCATE_OPTION                            .startsWith(nextWord)) configuration.obfuscate                        = parseNoArgument(false);
187            else if (ConfigurationConstants.PRINT_MAPPING_OPTION                             .startsWith(nextWord)) configuration.printMapping                     = parseOptionalFile();
188            else if (ConfigurationConstants.APPLY_MAPPING_OPTION                             .startsWith(nextWord)) configuration.applyMapping                     = parseFile();
189            else if (ConfigurationConstants.OBFUSCATION_DICTIONARY_OPTION                    .startsWith(nextWord)) configuration.obfuscationDictionary            = parseFile();
190            else if (ConfigurationConstants.CLASS_OBFUSCATION_DICTIONARY_OPTION              .startsWith(nextWord)) configuration.classObfuscationDictionary       = parseFile();
191            else if (ConfigurationConstants.PACKAGE_OBFUSCATION_DICTIONARY_OPTION            .startsWith(nextWord)) configuration.packageObfuscationDictionary     = parseFile();
192            else if (ConfigurationConstants.OVERLOAD_AGGRESSIVELY_OPTION                     .startsWith(nextWord)) configuration.overloadAggressively             = parseNoArgument(true);
193            else if (ConfigurationConstants.USE_UNIQUE_CLASS_MEMBER_NAMES_OPTION             .startsWith(nextWord)) configuration.useUniqueClassMemberNames        = parseNoArgument(true);
194            else if (ConfigurationConstants.DONT_USE_MIXED_CASE_CLASS_NAMES_OPTION           .startsWith(nextWord)) configuration.useMixedCaseClassNames           = parseNoArgument(false);
195            else if (ConfigurationConstants.KEEP_PACKAGE_NAMES_OPTION                        .startsWith(nextWord)) configuration.keepPackageNames                 = parseCommaSeparatedList("package name", true, true, false, false, true, false, true, false, configuration.keepPackageNames);
196            else if (ConfigurationConstants.FLATTEN_PACKAGE_HIERARCHY_OPTION                 .startsWith(nextWord)) configuration.flattenPackageHierarchy          = ClassUtil.internalClassName(parseOptionalArgument());
197            else if (ConfigurationConstants.REPACKAGE_CLASSES_OPTION                         .startsWith(nextWord)) configuration.repackageClasses                 = ClassUtil.internalClassName(parseOptionalArgument());
198            else if (ConfigurationConstants.DEFAULT_PACKAGE_OPTION                           .startsWith(nextWord)) configuration.repackageClasses                 = ClassUtil.internalClassName(parseOptionalArgument());
199            else if (ConfigurationConstants.KEEP_ATTRIBUTES_OPTION                           .startsWith(nextWord)) configuration.keepAttributes                   = parseCommaSeparatedList("attribute name", true, true, false, false, true, false, false, false, configuration.keepAttributes);
200            else if (ConfigurationConstants.KEEP_PARAMETER_NAMES_OPTION                      .startsWith(nextWord)) configuration.keepParameterNames               = parseNoArgument(true);
201            else if (ConfigurationConstants.RENAME_SOURCE_FILE_ATTRIBUTE_OPTION              .startsWith(nextWord)) configuration.newSourceFileAttribute           = parseOptionalArgument();
202            else if (ConfigurationConstants.ADAPT_CLASS_STRINGS_OPTION                       .startsWith(nextWord)) configuration.adaptClassStrings                = parseCommaSeparatedList("class name", true, true, false, false, true, false, true, false, configuration.adaptClassStrings);
203            else if (ConfigurationConstants.ADAPT_RESOURCE_FILE_NAMES_OPTION                 .startsWith(nextWord)) configuration.adaptResourceFileNames           = parseCommaSeparatedList("resource file name", true, true, false, true, false, false, false, false, configuration.adaptResourceFileNames);
204            else if (ConfigurationConstants.ADAPT_RESOURCE_FILE_CONTENTS_OPTION              .startsWith(nextWord)) configuration.adaptResourceFileContents        = parseCommaSeparatedList("resource file name", true, true, false, true, false, false, false, false, configuration.adaptResourceFileContents);
205
206            else if (ConfigurationConstants.DONT_PREVERIFY_OPTION                            .startsWith(nextWord)) configuration.preverify                        = parseNoArgument(false);
207            else if (ConfigurationConstants.MICRO_EDITION_OPTION                             .startsWith(nextWord)) configuration.microEdition                     = parseNoArgument(true);
208
209            else if (ConfigurationConstants.VERBOSE_OPTION                                   .startsWith(nextWord)) configuration.verbose                          = parseNoArgument(true);
210            else if (ConfigurationConstants.DONT_NOTE_OPTION                                 .startsWith(nextWord)) configuration.note                             = parseCommaSeparatedList("class name", true, true, false, false, true, false, true, false, configuration.note);
211            else if (ConfigurationConstants.DONT_WARN_OPTION                                 .startsWith(nextWord)) configuration.warn                             = parseCommaSeparatedList("class name", true, true, false, false, true, false, true, false, configuration.warn);
212            else if (ConfigurationConstants.IGNORE_WARNINGS_OPTION                           .startsWith(nextWord)) configuration.ignoreWarnings                   = parseNoArgument(true);
213            else if (ConfigurationConstants.PRINT_CONFIGURATION_OPTION                       .startsWith(nextWord)) configuration.printConfiguration               = parseOptionalFile();
214            else if (ConfigurationConstants.DUMP_OPTION                                      .startsWith(nextWord)) configuration.dump                             = parseOptionalFile();
215            else
216            {
217                throw new ParseException("Unknown option " + reader.locationDescription());
218            }
219        }
220    }
221
222
223
224    /**
225     * Closes the configuration.
226     * @throws IOException if an IO error occurs while closing the configuration.
227     */
228    public void close() throws IOException
229    {
230        if (reader != null)
231        {
232            reader.close();
233        }
234    }
235
236
237    private long parseIncludeArgument(long lastModified) throws ParseException, IOException
238    {
239        // Read the configuration file name.
240        readNextWord("configuration file name", true, false);
241
242        File file = file(nextWord);
243        reader.includeWordReader(new FileWordReader(file));
244
245        readNextWord();
246
247        return Math.max(lastModified, file.lastModified());
248    }
249
250
251    private void parseBaseDirectoryArgument() throws ParseException, IOException
252    {
253        // Read the base directory name.
254        readNextWord("base directory name", true, false);
255
256        reader.setBaseDir(file(nextWord));
257
258        readNextWord();
259    }
260
261
262    private ClassPath parseClassPathArgument(ClassPath classPath,
263                                             boolean   isOutput)
264    throws ParseException, IOException
265    {
266        // Create a new List if necessary.
267        if (classPath == null)
268        {
269            classPath = new ClassPath();
270        }
271
272        while (true)
273        {
274            // Read the next jar name.
275            readNextWord("jar or directory name", true, false);
276
277            // Create a new class path entry.
278            ClassPathEntry entry = new ClassPathEntry(file(nextWord), isOutput);
279
280            // Read the opening parenthesis or the separator, if any.
281            readNextWord();
282
283            // Read the optional filters.
284            if (!configurationEnd() &&
285                ConfigurationConstants.OPEN_ARGUMENTS_KEYWORD.equals(nextWord))
286            {
287                // Read all filters in an array.
288                List[] filters = new List[7];
289
290                int counter = 0;
291                do
292                {
293                    // Read the filter.
294                    filters[counter++] =
295                        parseCommaSeparatedList("filter", true, true, true, true, false, true, false, false, null);
296                }
297                while (counter < filters.length &&
298                       ConfigurationConstants.SEPARATOR_KEYWORD.equals(nextWord));
299
300                // Make sure there is a closing parenthesis.
301                if (!ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD.equals(nextWord))
302                {
303                    throw new ParseException("Expecting separating '" + ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD +
304                                             "' or '" + ConfigurationConstants.SEPARATOR_KEYWORD +
305                                             "', or closing '" + ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD +
306                                             "' before " + reader.locationDescription());
307                }
308
309                // Set all filters from the array on the entry.
310                entry.setFilter(filters[--counter]);
311                if (counter > 0)
312                {
313                    entry.setJarFilter(filters[--counter]);
314                    if (counter > 0)
315                    {
316                        entry.setWarFilter(filters[--counter]);
317                        if (counter > 0)
318                        {
319                            entry.setEarFilter(filters[--counter]);
320                            if (counter > 0)
321                            {
322                                entry.setZipFilter(filters[--counter]);
323                                if (counter > 0)
324                                {
325                                    // For backward compatibility, the apk
326                                    // filter comes second in the list.
327                                    entry.setApkFilter(filters[--counter]);
328                                    if (counter > 0)
329                                    {
330                                        // For backward compatibility, the aar
331                                        // filter comes first in the list.
332                                        entry.setAarFilter(filters[--counter]);
333                                    }
334                                }
335                            }
336                        }
337                    }
338                }
339
340                // Read the separator, if any.
341                readNextWord();
342            }
343
344            // Add the entry to the list.
345            classPath.add(entry);
346
347            if (configurationEnd())
348            {
349                return classPath;
350            }
351
352            if (!nextWord.equals(ConfigurationConstants.JAR_SEPARATOR_KEYWORD))
353            {
354                throw new ParseException("Expecting class path separator '" + ConfigurationConstants.JAR_SEPARATOR_KEYWORD +
355                                         "' before " + reader.locationDescription());
356            }
357        }
358    }
359
360
361    private int parseClassVersion()
362    throws ParseException, IOException
363    {
364        // Read the obligatory target.
365        readNextWord("java version");
366
367        int classVersion = ClassUtil.internalClassVersion(nextWord);
368        if (classVersion == 0)
369        {
370            throw new ParseException("Unsupported java version " + reader.locationDescription());
371        }
372
373        readNextWord();
374
375        return classVersion;
376    }
377
378
379    private int parseIntegerArgument()
380    throws ParseException, IOException
381    {
382        try
383        {
384            // Read the obligatory integer.
385            readNextWord("integer");
386
387            int integer = Integer.parseInt(nextWord);
388
389            readNextWord();
390
391            return integer;
392        }
393        catch (NumberFormatException e)
394        {
395            throw new ParseException("Expecting integer argument instead of '" + nextWord +
396                                     "' before " + reader.locationDescription());
397        }
398    }
399
400
401    private File parseFile()
402    throws ParseException, IOException
403    {
404        // Read the obligatory file name.
405        readNextWord("file name", true, false);
406
407        // Make sure the file is properly resolved.
408        File file = file(nextWord);
409
410        readNextWord();
411
412        return file;
413    }
414
415
416    private File parseOptionalFile()
417    throws ParseException, IOException
418    {
419        // Read the optional file name.
420        readNextWord(true);
421
422        // Didn't the user specify a file name?
423        if (configurationEnd())
424        {
425            return Configuration.STD_OUT;
426        }
427
428        // Make sure the file is properly resolved.
429        File file = file(nextWord);
430
431        readNextWord();
432
433        return file;
434    }
435
436
437    private String parseOptionalArgument() throws IOException
438    {
439        // Read the optional argument.
440        readNextWord();
441
442        // Didn't the user specify an argument?
443        if (configurationEnd())
444        {
445            return "";
446        }
447
448        String argument = nextWord;
449
450        readNextWord();
451
452        return argument;
453    }
454
455
456    private boolean parseNoArgument(boolean value) throws IOException
457    {
458        readNextWord();
459
460        return value;
461    }
462
463
464    private long parseNoArgument(long value) throws IOException
465    {
466        readNextWord();
467
468        return value;
469    }
470
471
472    private List parseKeepClassSpecificationArguments(List    keepClassSpecifications,
473                                                      boolean markClasses,
474                                                      boolean markConditionally,
475                                                      boolean allowShrinking)
476    throws ParseException, IOException
477    {
478        // Create a new List if necessary.
479        if (keepClassSpecifications == null)
480        {
481            keepClassSpecifications = new ArrayList();
482        }
483
484        boolean markDescriptorClasses = false;
485        //boolean allowShrinking        = false;
486        boolean allowOptimization     = false;
487        boolean allowObfuscation      = false;
488
489        // Read the keep modifiers.
490        while (true)
491        {
492            readNextWord("keyword '" + ConfigurationConstants.CLASS_KEYWORD +
493                         "', '"      + JavaConstants.ACC_INTERFACE +
494                         "', or '"   + JavaConstants.ACC_ENUM + "'",
495                         false, true);
496
497            if (!ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD.equals(nextWord))
498            {
499                // Not a comma. Stop parsing the keep modifiers.
500                break;
501            }
502
503            readNextWord("keyword '" + ConfigurationConstants.ALLOW_SHRINKING_SUBOPTION +
504                         "', '"      + ConfigurationConstants.ALLOW_OPTIMIZATION_SUBOPTION +
505                         "', or '"   + ConfigurationConstants.ALLOW_OBFUSCATION_SUBOPTION + "'");
506
507            if      (ConfigurationConstants.INCLUDE_DESCRIPTOR_CLASSES_SUBOPTION.startsWith(nextWord))
508            {
509                markDescriptorClasses = true;
510            }
511            else if (ConfigurationConstants.ALLOW_SHRINKING_SUBOPTION           .startsWith(nextWord))
512            {
513                allowShrinking        = true;
514            }
515            else if (ConfigurationConstants.ALLOW_OPTIMIZATION_SUBOPTION        .startsWith(nextWord))
516            {
517                allowOptimization     = true;
518            }
519            else if (ConfigurationConstants.ALLOW_OBFUSCATION_SUBOPTION         .startsWith(nextWord))
520            {
521                allowObfuscation      = true;
522            }
523            else
524            {
525                throw new ParseException("Expecting keyword '" + ConfigurationConstants.INCLUDE_DESCRIPTOR_CLASSES_SUBOPTION +
526                                         "', '"                + ConfigurationConstants.ALLOW_SHRINKING_SUBOPTION +
527                                         "', '"                + ConfigurationConstants.ALLOW_OPTIMIZATION_SUBOPTION +
528                                         "', or '"             + ConfigurationConstants.ALLOW_OBFUSCATION_SUBOPTION +
529                                         "' before " + reader.locationDescription());
530            }
531        }
532
533        // Read the class configuration.
534        ClassSpecification classSpecification =
535            parseClassSpecificationArguments();
536
537        // Create and add the keep configuration.
538        keepClassSpecifications.add(new KeepClassSpecification(markClasses,
539                                                               markConditionally,
540                                                               markDescriptorClasses,
541                                                               allowShrinking,
542                                                               allowOptimization,
543                                                               allowObfuscation,
544                                                               classSpecification));
545        return keepClassSpecifications;
546    }
547
548
549    private List parseClassSpecificationArguments(List classSpecifications)
550    throws ParseException, IOException
551    {
552        // Create a new List if necessary.
553        if (classSpecifications == null)
554        {
555            classSpecifications = new ArrayList();
556        }
557
558        // Read and add the class configuration.
559        readNextWord("keyword '" + ConfigurationConstants.CLASS_KEYWORD +
560                     "', '"      + JavaConstants.ACC_INTERFACE +
561                     "', or '"   + JavaConstants.ACC_ENUM + "'",
562                     false, true);
563
564        classSpecifications.add(parseClassSpecificationArguments());
565
566        return classSpecifications;
567    }
568
569
570    /**
571     * Parses and returns a class specification.
572     * @throws ParseException if the class specification contains a syntax error.
573     * @throws IOException    if an IO error occurs while reading the class
574     *                        specification.
575     */
576    public ClassSpecification parseClassSpecificationArguments()
577    throws ParseException, IOException
578    {
579        // Clear the annotation type.
580        String annotationType = null;
581
582        // Clear the class access modifiers.
583        int requiredSetClassAccessFlags   = 0;
584        int requiredUnsetClassAccessFlags = 0;
585
586        // Parse the class annotations and access modifiers until the class keyword.
587        while (!ConfigurationConstants.CLASS_KEYWORD.equals(nextWord))
588        {
589            // Strip the negating sign, if any.
590            boolean negated =
591                nextWord.startsWith(ConfigurationConstants.NEGATOR_KEYWORD);
592
593            String strippedWord = negated ?
594                nextWord.substring(1) :
595                nextWord;
596
597            // Parse the class access modifiers.
598            int accessFlag =
599                strippedWord.equals(JavaConstants.ACC_PUBLIC)     ? ClassConstants.ACC_PUBLIC      :
600                strippedWord.equals(JavaConstants.ACC_FINAL)      ? ClassConstants.ACC_FINAL       :
601                strippedWord.equals(JavaConstants.ACC_INTERFACE)  ? ClassConstants.ACC_INTERFACE   :
602                strippedWord.equals(JavaConstants.ACC_ABSTRACT)   ? ClassConstants.ACC_ABSTRACT    :
603                strippedWord.equals(JavaConstants.ACC_SYNTHETIC)  ? ClassConstants.ACC_SYNTHETIC   :
604                strippedWord.equals(JavaConstants.ACC_ANNOTATION) ? ClassConstants.ACC_ANNOTATTION :
605                strippedWord.equals(JavaConstants.ACC_ENUM)       ? ClassConstants.ACC_ENUM        :
606                                                                    unknownAccessFlag();
607
608            // Is it an annotation modifier?
609            if (accessFlag == ClassConstants.ACC_ANNOTATTION)
610            {
611                // Already read the next word.
612                readNextWord("annotation type or keyword '" + JavaConstants.ACC_INTERFACE + "'",
613                             false, false);
614
615                // Is the next word actually an annotation type?
616                if (!nextWord.equals(JavaConstants.ACC_INTERFACE) &&
617                    !nextWord.equals(JavaConstants.ACC_ENUM)      &&
618                    !nextWord.equals(ConfigurationConstants.CLASS_KEYWORD))
619                {
620                    // Parse the annotation type.
621                    annotationType =
622                        ListUtil.commaSeparatedString(
623                        parseCommaSeparatedList("annotation type",
624                                                false, false, false, false, true, false, false, true, null), false);
625
626                    // Continue parsing the access modifier that we just read
627                    // in the next cycle.
628                    continue;
629                }
630
631                // Otherwise just handle the annotation modifier.
632            }
633
634            if (!negated)
635            {
636                requiredSetClassAccessFlags   |= accessFlag;
637            }
638            else
639            {
640                requiredUnsetClassAccessFlags |= accessFlag;
641            }
642
643            if ((requiredSetClassAccessFlags &
644                 requiredUnsetClassAccessFlags) != 0)
645            {
646                throw new ParseException("Conflicting class access modifiers for '" + strippedWord +
647                                         "' before " + reader.locationDescription());
648            }
649
650            if (strippedWord.equals(JavaConstants.ACC_INTERFACE) ||
651                strippedWord.equals(JavaConstants.ACC_ENUM)      ||
652                strippedWord.equals(ConfigurationConstants.CLASS_KEYWORD))
653            {
654                // The interface or enum keyword. Stop parsing the class flags.
655                break;
656            }
657
658            // Should we read the next word?
659            if (accessFlag != ClassConstants.ACC_ANNOTATTION)
660            {
661                readNextWord("keyword '" + ConfigurationConstants.CLASS_KEYWORD +
662                             "', '"      + JavaConstants.ACC_INTERFACE +
663                             "', or '"   + JavaConstants.ACC_ENUM + "'",
664                             false, true);
665            }
666        }
667
668       // Parse the class name part.
669        String externalClassName =
670            ListUtil.commaSeparatedString(
671            parseCommaSeparatedList("class name or interface name",
672                                    true, false, false, false, true, false, false, false, null), false);
673
674        // For backward compatibility, allow a single "*" wildcard to match any
675        // class.
676        String className = ConfigurationConstants.ANY_CLASS_KEYWORD.equals(externalClassName) ?
677            null :
678            ClassUtil.internalClassName(externalClassName);
679
680        // Clear the annotation type and the class name of the extends part.
681        String extendsAnnotationType = null;
682        String extendsClassName      = null;
683
684        if (!configurationEnd())
685        {
686            // Parse 'implements ...' or 'extends ...' part, if any.
687            if (ConfigurationConstants.IMPLEMENTS_KEYWORD.equals(nextWord) ||
688                ConfigurationConstants.EXTENDS_KEYWORD.equals(nextWord))
689            {
690                readNextWord("class name or interface name", false, true);
691
692                // Parse the annotation type, if any.
693                if (ConfigurationConstants.ANNOTATION_KEYWORD.equals(nextWord))
694                {
695                    extendsAnnotationType =
696                        ListUtil.commaSeparatedString(
697                        parseCommaSeparatedList("annotation type",
698                                                true, false, false, false, true, false, false, true, null), false);
699                }
700
701                String externalExtendsClassName =
702                    ListUtil.commaSeparatedString(
703                    parseCommaSeparatedList("class name or interface name",
704                                            false, false, false, false, true, false, false, false, null), false);
705
706                extendsClassName = ConfigurationConstants.ANY_CLASS_KEYWORD.equals(externalExtendsClassName) ?
707                    null :
708                    ClassUtil.internalClassName(externalExtendsClassName);
709            }
710        }
711
712        // Create the basic class specification.
713        ClassSpecification classSpecification =
714            new ClassSpecification(lastComments,
715                                   requiredSetClassAccessFlags,
716                                   requiredUnsetClassAccessFlags,
717                                   annotationType,
718                                   className,
719                                   extendsAnnotationType,
720                                   extendsClassName);
721
722
723        // Now add any class members to this class specification.
724        if (!configurationEnd())
725        {
726            // Check the class member opening part.
727            if (!ConfigurationConstants.OPEN_KEYWORD.equals(nextWord))
728            {
729                throw new ParseException("Expecting opening '" + ConfigurationConstants.OPEN_KEYWORD +
730                                         "' at " + reader.locationDescription());
731            }
732
733            // Parse all class members.
734            while (true)
735            {
736                readNextWord("class member description" +
737                             " or closing '" + ConfigurationConstants.CLOSE_KEYWORD + "'",
738                             false, true);
739
740                if (nextWord.equals(ConfigurationConstants.CLOSE_KEYWORD))
741                {
742                    // The closing brace. Stop parsing the class members.
743                    readNextWord();
744
745                    break;
746                }
747
748                parseMemberSpecificationArguments(externalClassName,
749                                                  classSpecification);
750            }
751        }
752
753        return classSpecification;
754    }
755
756
757    private void parseMemberSpecificationArguments(String             externalClassName,
758                                                   ClassSpecification classSpecification)
759    throws ParseException, IOException
760    {
761        // Clear the annotation name.
762        String annotationType = null;
763
764        // Parse the class member access modifiers, if any.
765        int requiredSetMemberAccessFlags   = 0;
766        int requiredUnsetMemberAccessFlags = 0;
767
768        while (!configurationEnd(true))
769        {
770            // Parse the annotation type, if any.
771            if (ConfigurationConstants.ANNOTATION_KEYWORD.equals(nextWord))
772            {
773                annotationType =
774                    ListUtil.commaSeparatedString(
775                    parseCommaSeparatedList("annotation type",
776                                            true, false, false, false, true, false, false, true, null), false);
777                continue;
778            }
779
780            String strippedWord = nextWord.startsWith("!") ?
781                nextWord.substring(1) :
782                nextWord;
783
784            // Parse the class member access modifiers.
785            int accessFlag =
786                strippedWord.equals(JavaConstants.ACC_PUBLIC)       ? ClassConstants.ACC_PUBLIC       :
787                strippedWord.equals(JavaConstants.ACC_PRIVATE)      ? ClassConstants.ACC_PRIVATE      :
788                strippedWord.equals(JavaConstants.ACC_PROTECTED)    ? ClassConstants.ACC_PROTECTED    :
789                strippedWord.equals(JavaConstants.ACC_STATIC)       ? ClassConstants.ACC_STATIC       :
790                strippedWord.equals(JavaConstants.ACC_FINAL)        ? ClassConstants.ACC_FINAL        :
791                strippedWord.equals(JavaConstants.ACC_SYNCHRONIZED) ? ClassConstants.ACC_SYNCHRONIZED :
792                strippedWord.equals(JavaConstants.ACC_VOLATILE)     ? ClassConstants.ACC_VOLATILE     :
793                strippedWord.equals(JavaConstants.ACC_TRANSIENT)    ? ClassConstants.ACC_TRANSIENT    :
794                strippedWord.equals(JavaConstants.ACC_BRIDGE)       ? ClassConstants.ACC_BRIDGE       :
795                strippedWord.equals(JavaConstants.ACC_VARARGS)      ? ClassConstants.ACC_VARARGS      :
796                strippedWord.equals(JavaConstants.ACC_NATIVE)       ? ClassConstants.ACC_NATIVE       :
797                strippedWord.equals(JavaConstants.ACC_ABSTRACT)     ? ClassConstants.ACC_ABSTRACT     :
798                strippedWord.equals(JavaConstants.ACC_STRICT)       ? ClassConstants.ACC_STRICT       :
799                strippedWord.equals(JavaConstants.ACC_SYNTHETIC)    ? ClassConstants.ACC_SYNTHETIC    :
800                                                                      0;
801            if (accessFlag == 0)
802            {
803                // Not a class member access modifier. Stop parsing them.
804                break;
805            }
806
807            if (strippedWord.equals(nextWord))
808            {
809                requiredSetMemberAccessFlags   |= accessFlag;
810            }
811            else
812            {
813                requiredUnsetMemberAccessFlags |= accessFlag;
814            }
815
816            // Make sure the user doesn't try to set and unset the same
817            // access flags simultaneously.
818            if ((requiredSetMemberAccessFlags &
819                 requiredUnsetMemberAccessFlags) != 0)
820            {
821                throw new ParseException("Conflicting class member access modifiers for " +
822                                         reader.locationDescription());
823            }
824
825            readNextWord("class member description");
826        }
827
828        // Parse the class member type and name part.
829
830        // Did we get a special wildcard?
831        if (ConfigurationConstants.ANY_CLASS_MEMBER_KEYWORD.equals(nextWord) ||
832            ConfigurationConstants.ANY_FIELD_KEYWORD       .equals(nextWord) ||
833            ConfigurationConstants.ANY_METHOD_KEYWORD      .equals(nextWord))
834        {
835            // Act according to the type of wildcard..
836            if (ConfigurationConstants.ANY_CLASS_MEMBER_KEYWORD.equals(nextWord))
837            {
838                checkFieldAccessFlags(requiredSetMemberAccessFlags,
839                                      requiredUnsetMemberAccessFlags);
840                checkMethodAccessFlags(requiredSetMemberAccessFlags,
841                                       requiredUnsetMemberAccessFlags);
842
843                classSpecification.addField(
844                    new MemberSpecification(requiredSetMemberAccessFlags,
845                                            requiredUnsetMemberAccessFlags,
846                                            annotationType,
847                                            null,
848                                            null));
849                classSpecification.addMethod(
850                    new MemberSpecification(requiredSetMemberAccessFlags,
851                                            requiredUnsetMemberAccessFlags,
852                                            annotationType,
853                                            null,
854                                            null));
855            }
856            else if (ConfigurationConstants.ANY_FIELD_KEYWORD.equals(nextWord))
857            {
858                checkFieldAccessFlags(requiredSetMemberAccessFlags,
859                                      requiredUnsetMemberAccessFlags);
860
861                classSpecification.addField(
862                    new MemberSpecification(requiredSetMemberAccessFlags,
863                                            requiredUnsetMemberAccessFlags,
864                                            annotationType,
865                                            null,
866                                            null));
867            }
868            else if (ConfigurationConstants.ANY_METHOD_KEYWORD.equals(nextWord))
869            {
870                checkMethodAccessFlags(requiredSetMemberAccessFlags,
871                                       requiredUnsetMemberAccessFlags);
872
873                classSpecification.addMethod(
874                    new MemberSpecification(requiredSetMemberAccessFlags,
875                                            requiredUnsetMemberAccessFlags,
876                                            annotationType,
877                                            null,
878                                            null));
879            }
880
881            // We still have to read the closing separator.
882            readNextWord("separator '" + ConfigurationConstants.SEPARATOR_KEYWORD + "'");
883
884            if (!ConfigurationConstants.SEPARATOR_KEYWORD.equals(nextWord))
885            {
886                throw new ParseException("Expecting separator '" + ConfigurationConstants.SEPARATOR_KEYWORD +
887                                         "' before " + reader.locationDescription());
888            }
889        }
890        else
891        {
892            // Make sure we have a proper type.
893            checkJavaIdentifier("java type");
894            String type = nextWord;
895
896            readNextWord("class member name");
897            String name = nextWord;
898
899            // Did we get just one word before the opening parenthesis?
900            if (ConfigurationConstants.OPEN_ARGUMENTS_KEYWORD.equals(name))
901            {
902                // This must be a constructor then.
903                // Make sure the type is a proper constructor name.
904                if (!(type.equals(ClassConstants.METHOD_NAME_INIT) ||
905                      type.equals(externalClassName) ||
906                      type.equals(ClassUtil.externalShortClassName(externalClassName))))
907                {
908                    throw new ParseException("Expecting type and name " +
909                                             "instead of just '" + type +
910                                             "' before " + reader.locationDescription());
911                }
912
913                // Assign the fixed constructor type and name.
914                type = JavaConstants.TYPE_VOID;
915                name = ClassConstants.METHOD_NAME_INIT;
916            }
917            else
918            {
919                // It's not a constructor.
920                // Make sure we have a proper name.
921                checkJavaIdentifier("class member name");
922
923                // Read the opening parenthesis or the separating
924                // semi-colon.
925                readNextWord("opening '" + ConfigurationConstants.OPEN_ARGUMENTS_KEYWORD +
926                             "' or separator '" + ConfigurationConstants.SEPARATOR_KEYWORD + "'");
927            }
928
929            // Are we looking at a field, a method, or something else?
930            if (ConfigurationConstants.SEPARATOR_KEYWORD.equals(nextWord))
931            {
932                // It's a field.
933                checkFieldAccessFlags(requiredSetMemberAccessFlags,
934                                      requiredUnsetMemberAccessFlags);
935
936                // We already have a field descriptor.
937                String descriptor = ClassUtil.internalType(type);
938
939                // Add the field.
940                classSpecification.addField(
941                    new MemberSpecification(requiredSetMemberAccessFlags,
942                                            requiredUnsetMemberAccessFlags,
943                                            annotationType,
944                                            name,
945                                            descriptor));
946            }
947            else if (ConfigurationConstants.OPEN_ARGUMENTS_KEYWORD.equals(nextWord))
948            {
949                // It's a method.
950                checkMethodAccessFlags(requiredSetMemberAccessFlags,
951                                       requiredUnsetMemberAccessFlags);
952
953                // Parse the method arguments.
954                String descriptor =
955                    ClassUtil.internalMethodDescriptor(type,
956                                                       parseCommaSeparatedList("argument", true, true, true, false, true, false, false, false, null));
957
958                if (!ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD.equals(nextWord))
959                {
960                    throw new ParseException("Expecting separating '" + ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD +
961                                             "' or closing '" + ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD +
962                                             "' before " + reader.locationDescription());
963                }
964
965                // Read the separator after the closing parenthesis.
966                readNextWord("separator '" + ConfigurationConstants.SEPARATOR_KEYWORD + "'");
967
968                if (!ConfigurationConstants.SEPARATOR_KEYWORD.equals(nextWord))
969                {
970                    throw new ParseException("Expecting separator '" + ConfigurationConstants.SEPARATOR_KEYWORD +
971                                             "' before " + reader.locationDescription());
972                }
973
974                // Add the method.
975                classSpecification.addMethod(
976                    new MemberSpecification(requiredSetMemberAccessFlags,
977                                            requiredUnsetMemberAccessFlags,
978                                            annotationType,
979                                            name,
980                                            descriptor));
981            }
982            else
983            {
984                // It doesn't look like a field or a method.
985                throw new ParseException("Expecting opening '" + ConfigurationConstants.OPEN_ARGUMENTS_KEYWORD +
986                                         "' or separator '" + ConfigurationConstants.SEPARATOR_KEYWORD +
987                                         "' before " + reader.locationDescription());
988            }
989        }
990    }
991
992
993    /**
994     * Reads a comma-separated list of java identifiers or of file names.
995     * Examples of invocation arguments:
996     *   ("directory name", true,  true,  false, true,  false, true,  false, false, ...)
997     *   ("optimization",   true,  false, false, false, false, false, false, false, ...)
998     *   ("package name",   true,  true,  false, false, true,  false, true,  false, ...)
999     *   ("attribute name", true,  true,  false, false, true,  false, false, false, ...)
1000     *   ("class name",     true,  true,  false, false, true,  false, true,  false, ...)
1001     *   ("resource file",  true,  true,  false, true,  false, false, false, false, ...)
1002     *   ("resource file",  true,  true,  false, true,  false, false, false, false, ...)
1003     *   ("class name",     true,  true,  false, false, true,  false, true,  false, ...)
1004     *   ("class name",     true,  true,  false, false, true,  false, true,  false, ...)
1005     *   ("filter",         true,  true,  true,  true,  false, true,  false, false, ...)
1006     *   ("annotation ",    false, false, false, false, true,  false, false, true,  ...)
1007     *   ("class name ",    true,  false, false, false, true,  false, false, false, ...)
1008     *   ("annotation ",    true,  false, false, false, true,  false, false, true,  ...)
1009     *   ("class name ",    false, false, false, false, true,  false, false, false, ...)
1010     *   ("annotation ",    true,  false, false, false, true,  false, false, true,  ...)
1011     *   ("argument",       true,  true,  true,  false, true,  false, false, false, ...)
1012     */
1013    private List parseCommaSeparatedList(String  expectedDescription,
1014                                         boolean readFirstWord,
1015                                         boolean allowEmptyList,
1016                                         boolean expectClosingParenthesis,
1017                                         boolean isFileName,
1018                                         boolean checkJavaIdentifiers,
1019                                         boolean replaceSystemProperties,
1020                                         boolean replaceExternalClassNames,
1021                                         boolean replaceExternalTypes,
1022                                         List    list)
1023    throws ParseException, IOException
1024    {
1025        if (list == null)
1026        {
1027            list = new ArrayList();
1028        }
1029
1030        if (readFirstWord)
1031        {
1032            if (!allowEmptyList)
1033            {
1034                // Read the first list entry.
1035                readNextWord(expectedDescription, isFileName, false);
1036            }
1037            else if (expectClosingParenthesis)
1038            {
1039                // Read the first list entry.
1040                readNextWord(expectedDescription, isFileName, false);
1041
1042                // Return if the entry is actually empty (an empty file name or
1043                // a closing parenthesis).
1044                if (nextWord.length() == 0)
1045                {
1046                    // Read the closing parenthesis
1047                    readNextWord("closing '" + ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD +
1048                                 "'");
1049
1050                    return list;
1051                }
1052                else if (nextWord.equals(ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD))
1053                {
1054                    return list;
1055                }
1056            }
1057            else
1058            {
1059                // Read the first list entry, if there is any.
1060                readNextWord(isFileName);
1061
1062                // Check if the list is empty.
1063                if (configurationEnd())
1064                {
1065                    return list;
1066                }
1067            }
1068        }
1069
1070        while (true)
1071        {
1072            if (checkJavaIdentifiers)
1073            {
1074                checkJavaIdentifier("java type");
1075            }
1076
1077            if (replaceSystemProperties)
1078            {
1079                nextWord = replaceSystemProperties(nextWord);
1080            }
1081
1082            if (replaceExternalClassNames)
1083            {
1084                nextWord = ClassUtil.internalClassName(nextWord);
1085            }
1086
1087            if (replaceExternalTypes)
1088            {
1089                nextWord = ClassUtil.internalType(nextWord);
1090            }
1091
1092            list.add(nextWord);
1093
1094            if (expectClosingParenthesis)
1095            {
1096                // Read a comma (or a closing parenthesis, or a different word).
1097                readNextWord("separating '" + ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD +
1098                             "' or closing '" + ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD +
1099                             "'");
1100            }
1101            else
1102            {
1103                // Read a comma (or a different word).
1104                readNextWord();
1105            }
1106
1107            if (!ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD.equals(nextWord))
1108            {
1109                return list;
1110            }
1111
1112            // Read the next list entry.
1113            readNextWord(expectedDescription, isFileName, false);
1114        }
1115    }
1116
1117
1118    /**
1119     * Throws a ParseException for an unexpected keyword.
1120     */
1121    private int unknownAccessFlag() throws ParseException
1122    {
1123        throw new ParseException("Unexpected keyword " + reader.locationDescription());
1124    }
1125
1126
1127    /**
1128     * Creates a properly resolved File, based on the given word.
1129     */
1130    private File file(String word) throws ParseException
1131    {
1132        String fileName = replaceSystemProperties(word);
1133        File   file     = new File(fileName);
1134
1135        // Try to get an absolute file.
1136        if (!file.isAbsolute())
1137        {
1138            file = new File(reader.getBaseDir(), fileName);
1139        }
1140
1141        return file;
1142    }
1143
1144
1145    /**
1146     * Replaces any properties in the given word by their values.
1147     * For instance, the substring "<java.home>" is replaced by its value.
1148     */
1149    private String replaceSystemProperties(String word) throws ParseException
1150    {
1151        int fromIndex = 0;
1152        while (true)
1153        {
1154            fromIndex = word.indexOf(ConfigurationConstants.OPEN_SYSTEM_PROPERTY, fromIndex);
1155            if (fromIndex < 0)
1156            {
1157                break;
1158            }
1159
1160            int toIndex = word.indexOf(ConfigurationConstants.CLOSE_SYSTEM_PROPERTY, fromIndex+1);
1161            if (toIndex < 0)
1162            {
1163                break;
1164            }
1165
1166            String propertyName  = word.substring(fromIndex+1, toIndex);
1167            String propertyValue = properties.getProperty(propertyName);
1168            if (propertyValue == null)
1169            {
1170                throw new ParseException("Value of system property '" + propertyName +
1171                                         "' is undefined in " + reader.locationDescription());
1172            }
1173
1174            word = word.substring(0, fromIndex) + propertyValue + word.substring(toIndex+1);
1175
1176            fromIndex += propertyValue.length();
1177        }
1178
1179        return word;
1180    }
1181
1182
1183    /**
1184     * Reads the next word of the configuration in the 'nextWord' field,
1185     * throwing an exception if there is no next word.
1186     */
1187    private void readNextWord(String expectedDescription)
1188    throws ParseException, IOException
1189    {
1190        readNextWord(expectedDescription, false, false);
1191    }
1192
1193
1194    /**
1195     * Reads the next word of the configuration in the 'nextWord' field,
1196     * throwing an exception if there is no next word.
1197     */
1198    private void readNextWord(String  expectedDescription,
1199                              boolean isFileName,
1200                              boolean expectingAtCharacter)
1201    throws ParseException, IOException
1202    {
1203        readNextWord(isFileName);
1204        if (configurationEnd(expectingAtCharacter))
1205        {
1206            throw new ParseException("Expecting " + expectedDescription +
1207                                     " before " + reader.locationDescription());
1208        }
1209    }
1210
1211
1212    /**
1213     * Reads the next word of the configuration in the 'nextWord' field.
1214     */
1215    private void readNextWord() throws IOException
1216    {
1217        readNextWord(false);
1218    }
1219
1220
1221    /**
1222     * Reads the next word of the configuration in the 'nextWord' field.
1223     */
1224    private void readNextWord(boolean isFileName) throws IOException
1225    {
1226        nextWord = reader.nextWord(isFileName);
1227    }
1228
1229
1230    /**
1231     * Returns whether the end of the configuration has been reached.
1232     */
1233    private boolean configurationEnd()
1234    {
1235        return configurationEnd(false);
1236    }
1237
1238
1239    /**
1240     * Returns whether the end of the configuration has been reached.
1241     */
1242    private boolean configurationEnd(boolean expectingAtCharacter)
1243    {
1244        return nextWord == null ||
1245               nextWord.startsWith(ConfigurationConstants.OPTION_PREFIX) ||
1246               (!expectingAtCharacter &&
1247                nextWord.equals(ConfigurationConstants.AT_DIRECTIVE));
1248    }
1249
1250
1251    /**
1252     * Checks whether the given word is a valid Java identifier and throws
1253     * a ParseException if it isn't. Wildcard characters are accepted.
1254     */
1255    private void checkJavaIdentifier(String expectedDescription)
1256    throws ParseException
1257    {
1258        if (!isJavaIdentifier(nextWord))
1259        {
1260            throw new ParseException("Expecting " + expectedDescription +
1261                                     " before " + reader.locationDescription());
1262        }
1263    }
1264
1265
1266    /**
1267     * Returns whether the given word is a valid Java identifier.
1268     * Wildcard characters are accepted.
1269     */
1270    private boolean isJavaIdentifier(String aWord)
1271    {
1272        if (aWord.length() == 0)
1273        {
1274            return false;
1275        }
1276
1277        for (int index = 0; index < aWord.length(); index++)
1278        {
1279            char c = aWord.charAt(index);
1280            if (!(Character.isJavaIdentifierPart(c) ||
1281                  c == '.' ||
1282                  c == '[' ||
1283                  c == ']' ||
1284                  c == '<' ||
1285                  c == '>' ||
1286                  c == '-' ||
1287                  c == '!' ||
1288                  c == '*' ||
1289                  c == '?' ||
1290                  c == '%'))
1291            {
1292                return false;
1293            }
1294        }
1295
1296        return true;
1297    }
1298
1299
1300    /**
1301     * Checks whether the given access flags are valid field access flags,
1302     * throwing a ParseException if they aren't.
1303     */
1304    private void checkFieldAccessFlags(int requiredSetMemberAccessFlags,
1305                                       int requiredUnsetMemberAccessFlags)
1306    throws ParseException
1307    {
1308        if (((requiredSetMemberAccessFlags |
1309              requiredUnsetMemberAccessFlags) &
1310            ~ClassConstants.VALID_ACC_FIELD) != 0)
1311        {
1312            throw new ParseException("Invalid method access modifier for field before " +
1313                                     reader.locationDescription());
1314        }
1315    }
1316
1317
1318    /**
1319     * Checks whether the given access flags are valid method access flags,
1320     * throwing a ParseException if they aren't.
1321     */
1322    private void checkMethodAccessFlags(int requiredSetMemberAccessFlags,
1323                                        int requiredUnsetMemberAccessFlags)
1324    throws ParseException
1325    {
1326        if (((requiredSetMemberAccessFlags |
1327              requiredUnsetMemberAccessFlags) &
1328            ~ClassConstants.VALID_ACC_METHOD) != 0)
1329        {
1330            throw new ParseException("Invalid field access modifier for method before " +
1331                                     reader.locationDescription());
1332        }
1333    }
1334
1335
1336    /**
1337     * A main method for testing configuration parsing.
1338     */
1339    public static void main(String[] args)
1340    {
1341        try
1342        {
1343            ConfigurationParser parser =
1344                new ConfigurationParser(args, System.getProperties());
1345
1346            try
1347            {
1348                parser.parse(new Configuration());
1349            }
1350            catch (ParseException ex)
1351            {
1352                ex.printStackTrace();
1353            }
1354            finally
1355            {
1356                parser.close();
1357            }
1358        }
1359        catch (IOException ex)
1360        {
1361            ex.printStackTrace();
1362        }
1363    }
1364}
1365