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