All Downloads are FREE. Search and download functionalities are using the official Maven repository.

proguard.ConfigurationParser Maven / Gradle / Ivy

Go to download

ProGuard is a free shrinker, optimizer, obfuscator, and preverifier for Java bytecode

There is a newer version: 7.6.0
Show newest version
/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification
 *             of Java bytecode.
 *
 * Copyright (c) 2002-2020 Guardsquare NV
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package proguard;

import proguard.classfile.*;
import proguard.classfile.util.ClassUtil;
import proguard.util.*;

import java.io.*;
import java.net.*;
import java.util.*;


/**
 * This class parses ProGuard configurations. Configurations can be read from an
 * array of arguments or from a configuration file or URL. External references
 * in file names ('<...>') can be resolved against a given set of properties.
 *
 * @author Eric Lafortune
 */
public class ConfigurationParser implements AutoCloseable
{
    private final WordReader reader;
    private final Properties properties;

    private String     nextWord;
    private String     lastComments;


    /**
     * Creates a new ConfigurationParser for the given String arguments and
     * the given Properties.
     */
    public ConfigurationParser(String[]   args,
                               Properties properties) throws IOException
    {
        this(args, null, properties);
    }


    /**
     * Creates a new ConfigurationParser for the given String arguments,
     * with the given base directory and the given Properties.
     */
    public ConfigurationParser(String[]   args,
                               File       baseDir,
                               Properties properties) throws IOException
    {
        this(new ArgumentWordReader(args, baseDir), properties);
    }


    /**
     * Creates a new ConfigurationParser for the given lines,
     * with the given base directory and the given Properties.
     */
    public ConfigurationParser(String     lines,
                               String     description,
                               File       baseDir,
                               Properties properties) throws IOException
    {
        this(new LineWordReader(new LineNumberReader(new StringReader(lines)),
                                description,
                                baseDir),
             properties);
    }


    /**
     * Creates a new ConfigurationParser for the given file, with the system
     * Properties.
     * @deprecated Temporary code for backward compatibility in Obclipse.
     */
    public ConfigurationParser(File file) throws IOException
    {
        this(file, System.getProperties());
    }


    /**
     * Creates a new ConfigurationParser for the given file and the given
     * Properties.
     */
    public ConfigurationParser(File       file,
                               Properties properties) throws IOException
    {
        this(new FileWordReader(file), properties);
    }


    /**
     * Creates a new ConfigurationParser for the given URL and the given
     * Properties.
     */
    public ConfigurationParser(URL        url,
                               Properties properties) throws IOException
    {
        this(new FileWordReader(url), properties);
    }


    /**
     * Creates a new ConfigurationParser for the given word reader and the
     * given Properties.
     */
    public ConfigurationParser(WordReader reader,
                               Properties properties) throws IOException
    {
        this.reader     = reader;
        this.properties = properties;

        readNextWord();
    }


    /**
     * Parses and returns the configuration.
     * @param configuration the configuration that is updated as a side-effect.
     * @throws ParseException if the any of the configuration settings contains
     *                        a syntax error.
     * @throws IOException if an IO error occurs while reading a configuration.
     */
    public void parse(Configuration configuration)
    throws ParseException, IOException
    {
        while (nextWord != null)
        {
            lastComments = reader.lastComments();

            // First include directives.
            if      (ConfigurationConstants.AT_DIRECTIVE                                     .startsWith(nextWord) ||
                     ConfigurationConstants.INCLUDE_DIRECTIVE                                .startsWith(nextWord)) configuration.lastModified                          = parseIncludeArgument(configuration.lastModified);
            else if (ConfigurationConstants.BASE_DIRECTORY_DIRECTIVE                         .startsWith(nextWord)) parseBaseDirectoryArgument();

            // Then configuration options with or without arguments.
            else if (ConfigurationConstants.INJARS_OPTION                                    .startsWith(nextWord)) configuration.programJars                           = parseClassPathArgument(configuration.programJars, false, true);
            else if (ConfigurationConstants.OUTJARS_OPTION                                   .startsWith(nextWord)) configuration.programJars                           = parseClassPathArgument(configuration.programJars, true, false);
            else if (ConfigurationConstants.LIBRARYJARS_OPTION                               .startsWith(nextWord)) configuration.libraryJars                           = parseClassPathArgument(configuration.libraryJars, false, false);
            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");
            else if (ConfigurationConstants.SKIP_NON_PUBLIC_LIBRARY_CLASSES_OPTION           .startsWith(nextWord)) configuration.skipNonPublicLibraryClasses           = parseNoArgument(true);
            else if (ConfigurationConstants.DONT_SKIP_NON_PUBLIC_LIBRARY_CLASSES_OPTION      .startsWith(nextWord)) configuration.skipNonPublicLibraryClasses           = parseNoArgument(false);
            else if (ConfigurationConstants.DONT_SKIP_NON_PUBLIC_LIBRARY_CLASS_MEMBERS_OPTION.startsWith(nextWord)) configuration.skipNonPublicLibraryClassMembers      = parseNoArgument(false);
            else if (ConfigurationConstants.TARGET_OPTION                                    .startsWith(nextWord)) configuration.targetClassVersion                    = parseClassVersion();
            else if (ConfigurationConstants.DONT_COMPRESS_OPTION                             .startsWith(nextWord)) configuration.dontCompress                          = parseCommaSeparatedList("file name", true, true, false, true, false, true, false, false, false, configuration.dontCompress);
            else if (ConfigurationConstants.ZIP_ALIGN_OPTION                                 .startsWith(nextWord)) configuration.zipAlign                              = parseIntegerArgument();
            else if (ConfigurationConstants.FORCE_PROCESSING_OPTION                          .startsWith(nextWord)) configuration.lastModified                          = parseNoArgument(Long.MAX_VALUE);

            else if (ConfigurationConstants.IF_OPTION                                        .startsWith(nextWord)) configuration.keep                                  = parseIfCondition(configuration.keep);
            else if (ConfigurationConstants.KEEP_OPTION                                      .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, true,  true,  false, false, false, null);
            else if (ConfigurationConstants.KEEP_CLASS_MEMBERS_OPTION                        .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, false, true,  false, false, false, null);
            else if (ConfigurationConstants.KEEP_CLASSES_WITH_MEMBERS_OPTION                 .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, false, true,  false, true,  false, null);
            else if (ConfigurationConstants.KEEP_NAMES_OPTION                                .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, true,  true,  false, false, true,  null);
            else if (ConfigurationConstants.KEEP_CLASS_MEMBER_NAMES_OPTION                   .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, false, true,  false, false, true,  null);
            else if (ConfigurationConstants.KEEP_CLASSES_WITH_MEMBER_NAMES_OPTION            .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, false, true,  false, true,  true,  null);
            else if (ConfigurationConstants.KEEP_CODE_OPTION                                 .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, false, false, true,  false, false, null);
            else if (ConfigurationConstants.PRINT_SEEDS_OPTION                               .startsWith(nextWord)) configuration.printSeeds                            = parseOptionalFile();

            // After '-keep'.
            else if (ConfigurationConstants.KEEP_DIRECTORIES_OPTION                          .startsWith(nextWord)) configuration.keepDirectories                       = parseCommaSeparatedList("directory name", true, true, false, true, false, true, true, false, false, configuration.keepDirectories);

            else if (ConfigurationConstants.DONT_SHRINK_OPTION                               .startsWith(nextWord)) configuration.shrink                                = parseNoArgument(false);
            else if (ConfigurationConstants.PRINT_USAGE_OPTION                               .startsWith(nextWord)) configuration.printUsage                            = parseOptionalFile();
            else if (ConfigurationConstants.WHY_ARE_YOU_KEEPING_OPTION                       .startsWith(nextWord)) configuration.whyAreYouKeeping                      = parseClassSpecificationArguments(configuration.whyAreYouKeeping);

            else if (ConfigurationConstants.DONT_OPTIMIZE_OPTION                             .startsWith(nextWord)) configuration.optimize                              = parseNoArgument(false);
            else if (ConfigurationConstants.OPTIMIZATION_PASSES                              .startsWith(nextWord)) configuration.optimizationPasses                    = parseIntegerArgument();
            else if (ConfigurationConstants.OPTIMIZATIONS                                    .startsWith(nextWord)) configuration.optimizations                         = parseCommaSeparatedList("optimization name", true, false, false, false, false, true, false, false, false, configuration.optimizations);
            else if (ConfigurationConstants.ASSUME_NO_SIDE_EFFECTS_OPTION                    .startsWith(nextWord)) configuration.assumeNoSideEffects                   = parseAssumeClassSpecificationArguments(configuration.assumeNoSideEffects);
            else if (ConfigurationConstants.ASSUME_NO_EXTERNAL_SIDE_EFFECTS_OPTION           .startsWith(nextWord)) configuration.assumeNoExternalSideEffects           = parseAssumeClassSpecificationArguments(configuration.assumeNoExternalSideEffects);
            else if (ConfigurationConstants.ASSUME_NO_ESCAPING_PARAMETERS_OPTION             .startsWith(nextWord)) configuration.assumeNoEscapingParameters            = parseAssumeClassSpecificationArguments(configuration.assumeNoEscapingParameters);
            else if (ConfigurationConstants.ASSUME_NO_EXTERNAL_RETURN_VALUES_OPTION          .startsWith(nextWord)) configuration.assumeNoExternalReturnValues          = parseAssumeClassSpecificationArguments(configuration.assumeNoExternalReturnValues);
            else if (ConfigurationConstants.ASSUME_VALUES_OPTION                             .startsWith(nextWord)) configuration.assumeValues                          = parseAssumeClassSpecificationArguments(configuration.assumeValues);
            else if (ConfigurationConstants.ALLOW_ACCESS_MODIFICATION_OPTION                 .startsWith(nextWord)) configuration.allowAccessModification               = parseNoArgument(true);
            else if (ConfigurationConstants.MERGE_INTERFACES_AGGRESSIVELY_OPTION             .startsWith(nextWord)) configuration.mergeInterfacesAggressively           = parseNoArgument(true);

            else if (ConfigurationConstants.DONT_OBFUSCATE_OPTION                            .startsWith(nextWord)) configuration.obfuscate                             = parseNoArgument(false);
            else if (ConfigurationConstants.PRINT_MAPPING_OPTION                             .startsWith(nextWord)) configuration.printMapping                          = parseOptionalFile();
            else if (ConfigurationConstants.APPLY_MAPPING_OPTION                             .startsWith(nextWord)) configuration.applyMapping                          = parseFile();
            else if (ConfigurationConstants.OBFUSCATION_DICTIONARY_OPTION                    .startsWith(nextWord)) configuration.obfuscationDictionary                 = parseURL();
            else if (ConfigurationConstants.CLASS_OBFUSCATION_DICTIONARY_OPTION              .startsWith(nextWord)) configuration.classObfuscationDictionary            = parseURL();
            else if (ConfigurationConstants.PACKAGE_OBFUSCATION_DICTIONARY_OPTION            .startsWith(nextWord)) configuration.packageObfuscationDictionary          = parseURL();
            else if (ConfigurationConstants.OVERLOAD_AGGRESSIVELY_OPTION                     .startsWith(nextWord)) configuration.overloadAggressively                  = parseNoArgument(true);
            else if (ConfigurationConstants.USE_UNIQUE_CLASS_MEMBER_NAMES_OPTION             .startsWith(nextWord)) configuration.useUniqueClassMemberNames             = parseNoArgument(true);
            else if (ConfigurationConstants.DONT_USE_MIXED_CASE_CLASS_NAMES_OPTION           .startsWith(nextWord)) configuration.useMixedCaseClassNames                = parseNoArgument(false);
            else if (ConfigurationConstants.KEEP_PACKAGE_NAMES_OPTION                        .startsWith(nextWord)) configuration.keepPackageNames                      = parseCommaSeparatedList("package name", true, true, false, false, true, false, false, true, false, configuration.keepPackageNames);
            else if (ConfigurationConstants.FLATTEN_PACKAGE_HIERARCHY_OPTION                 .startsWith(nextWord)) configuration.flattenPackageHierarchy               = ClassUtil.internalClassName(parseOptionalArgument());
            else if (ConfigurationConstants.REPACKAGE_CLASSES_OPTION                         .startsWith(nextWord) ||
                     ConfigurationConstants.DEFAULT_PACKAGE_OPTION                           .startsWith(nextWord)) configuration.repackageClasses                      = ClassUtil.internalClassName(parseOptionalArgument());
            else if (ConfigurationConstants.KEEP_ATTRIBUTES_OPTION                           .startsWith(nextWord)) configuration.keepAttributes                        = parseCommaSeparatedList("attribute name", true, true, false, false, true, false, false, false, false, configuration.keepAttributes);
            else if (ConfigurationConstants.KEEP_PARAMETER_NAMES_OPTION                      .startsWith(nextWord)) configuration.keepParameterNames                    = parseNoArgument(true);
            else if (ConfigurationConstants.RENAME_SOURCE_FILE_ATTRIBUTE_OPTION              .startsWith(nextWord)) configuration.newSourceFileAttribute                = parseOptionalArgument();
            else if (ConfigurationConstants.ADAPT_CLASS_STRINGS_OPTION                       .startsWith(nextWord)) configuration.adaptClassStrings                     = parseCommaSeparatedList("class name", true, true, false, false, true, false, false, true, false, configuration.adaptClassStrings);
            else if (ConfigurationConstants.ADAPT_RESOURCE_FILE_NAMES_OPTION                 .startsWith(nextWord)) configuration.adaptResourceFileNames                = parseCommaSeparatedList("resource file name", true, true, false, true, false, true, false, false, false, configuration.adaptResourceFileNames);
            else if (ConfigurationConstants.ADAPT_RESOURCE_FILE_CONTENTS_OPTION              .startsWith(nextWord)) configuration.adaptResourceFileContents             = parseCommaSeparatedList("resource file name", true, true, false, true, false, true, false, false, false, configuration.adaptResourceFileContents);
            else if (ConfigurationConstants.DONT_PROCESS_KOTLIN_METADATA                     .startsWith(nextWord)) configuration.dontProcessKotlinMetadata             = parseNoArgument(true);
            else if (ConfigurationConstants.KEEP_KOTLIN_METADATA                             .startsWith(nextWord)) configuration.keepKotlinMetadata                    = parseKeepKotlinMetadata();

            else if (ConfigurationConstants.DONT_PREVERIFY_OPTION                            .startsWith(nextWord)) configuration.preverify                             = parseNoArgument(false);
            else if (ConfigurationConstants.MICRO_EDITION_OPTION                             .startsWith(nextWord)) configuration.microEdition                          = parseNoArgument(true);
            else if (ConfigurationConstants.ANDROID_OPTION                                   .startsWith(nextWord)) configuration.android                               = parseNoArgument(true);

            else if (ConfigurationConstants.KEY_STORE_OPTION                                 .startsWith(nextWord)) configuration.keyStores                             = parseFiles(configuration.keyStores);
            else if (ConfigurationConstants.KEY_STORE_PASSWORD_OPTION                        .startsWith(nextWord)) configuration.keyStorePasswords                     = parseCommaSeparatedList("keystore password", true, false, false, false, false, false, true, false, false, configuration.keyStorePasswords);
            else if (ConfigurationConstants.KEY_ALIAS_OPTION                                 .startsWith(nextWord)) configuration.keyAliases                            = parseCommaSeparatedList("key", true, false, false, false, false, false, true, false, false, configuration.keyAliases);
            else if (ConfigurationConstants.KEY_PASSWORD_OPTION                              .startsWith(nextWord)) configuration.keyPasswords                          = parseCommaSeparatedList("key password", true, false, false, false, false, false, true, false, false, configuration.keyPasswords);

            else if (ConfigurationConstants.VERBOSE_OPTION                                   .startsWith(nextWord)) configuration.verbose                               = parseNoArgument(true);
            else if (ConfigurationConstants.DONT_NOTE_OPTION                                 .startsWith(nextWord)) configuration.note                                  = parseCommaSeparatedList("class name", true, true, false, false, true, false, false, true, false, configuration.note);
            else if (ConfigurationConstants.DONT_WARN_OPTION                                 .startsWith(nextWord)) configuration.warn                                  = parseCommaSeparatedList("class name", true, true, false, false, true, false, false, true, false, configuration.warn);
            else if (ConfigurationConstants.IGNORE_WARNINGS_OPTION                           .startsWith(nextWord)) configuration.ignoreWarnings                        = parseNoArgument(true);
            else if (ConfigurationConstants.PRINT_CONFIGURATION_OPTION                       .startsWith(nextWord)) configuration.printConfiguration                    = parseOptionalFile();
            else if (ConfigurationConstants.DUMP_OPTION                                      .startsWith(nextWord)) configuration.dump                                  = parseOptionalFile();
            else if (ConfigurationConstants.ADD_CONFIGURATION_DEBUGGING_OPTION               .startsWith(nextWord)) configuration.addConfigurationDebugging             = parseNoArgument(true);
            else if (ConfigurationConstants.OPTIMIZE_AGGRESSIVELY                            .startsWith(nextWord)) configuration.optimizeConservatively                = parseNoArgument(false);
            else
            {
                throw new ParseException("Unknown option " + reader.locationDescription());
            }
        }
    }


    /**
     * Closes the configuration.
     * @throws IOException if an IO error occurs while closing the configuration.
     */
    @Override
    public void close() throws IOException
    {
        if (reader != null)
        {
            reader.close();
        }
    }


    private boolean parseKeepKotlinMetadata() throws IOException
    {
        System.err.println("The `-keepkotlinmetadata` option is deprecated and will be removed in a future ProGuard release." +
                           "Please use `-keep class kotlin.Metadata` instead.");
        return parseNoArgument(true);
    }


    private long parseIncludeArgument(long lastModified) throws ParseException, IOException
    {
        // Read the configuration file name.
        readNextWord("configuration file name", true, true, false);

        URL baseURL = reader.getBaseURL();
        URL url     = null;

        try
        {
            // Check if the file name is a valid URL.
            url = new URL(nextWord);
        }
        catch (MalformedURLException ex) {}

        if (url != null)
        {
            reader.includeWordReader(new FileWordReader(url));
        }
        // Is it relative to a URL or to a file?
        else if (baseURL != null)
        {
            url = new URL(baseURL, nextWord);
            reader.includeWordReader(new FileWordReader(url));
        }
        else
        {
            // Is the file a valid resource URL?
            url = ConfigurationParser.class.getResource(nextWord);
            if (url != null)
            {
                reader.includeWordReader(new FileWordReader(url));
            }
            else
            {
                File file = file(nextWord);
                reader.includeWordReader(new FileWordReader(file));

                long fileLastModified = file.lastModified();
                if (fileLastModified > lastModified)
                {
                    lastModified = fileLastModified;
                }
            }
        }

        readNextWord();

        return lastModified;
    }


    private void parseBaseDirectoryArgument() throws ParseException, IOException
    {
        // Read the base directory name.
        readNextWord("base directory name", true, true, false);

        reader.setBaseDir(file(nextWord));

        readNextWord();
    }


    private ClassPath parseClassPathArgument(ClassPath classPath,
                                             boolean   isOutput,
                                             boolean   allowFeatureName)
    throws ParseException, IOException
    {
        // Create a new List if necessary.
        if (classPath == null)
        {
            classPath = new ClassPath();
        }

        // Read the first jar name.
        readNextWord("jar or directory name", true, false, false);

        // Do we have a suboption with a feature name instead?
        // The file name then comes out as an empty string.
        String featureName = null;

        if (allowFeatureName &&
            nextWord.length() == 0)
        {
            // Read the separator.
            readNextWord("separating '" + ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD + "'");

            if (!ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD.equals(nextWord))
            {
                throw new ParseException("Expecting separating '" + ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD +
                                         "' or jar or directory name" +
                                         "' before " + reader.locationDescription());
            }

            // Read and remember the feature name.
            readNextWord("feature name", false, false, false);

            featureName = nextWord;

            // Read the first jar name.
            readNextWord("jar or directory name", true, false, false);
        }

        while (true)
        {
            // Create a new class path entry.
            ClassPathEntry entry = new ClassPathEntry(file(nextWord),
                                                      isOutput,
                                                      featureName);

            // Read the opening parenthesis or the separator, if any.
            readNextWord();

            // Read the optional filters.
            if (!configurationEnd() &&
                ConfigurationConstants.OPEN_ARGUMENTS_KEYWORD.equals(nextWord))
            {
                // Read all filters in an array.
                List[] filters = new List[7];

                int counter = 0;
                do
                {
                    // Read the filter.
                    filters[counter++] =
                        parseCommaSeparatedList("filter", true, true, true, true, false, true, true, false, false, null);
                }
                while (counter < filters.length &&
                       ConfigurationConstants.SEPARATOR_KEYWORD.equals(nextWord));

                // Make sure there is a closing parenthesis.
                if (!ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD.equals(nextWord))
                {
                    throw new ParseException("Expecting separating '" + ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD +
                                             "' or '" + ConfigurationConstants.SEPARATOR_KEYWORD +
                                             "', or closing '" + ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD +
                                             "' before " + reader.locationDescription());
                }

                // Set all filters from the array on the entry.
                entry.setFilter(filters[--counter]);
                if (counter > 0)
                {
                    entry.setJarFilter(filters[--counter]);
                    if (counter > 0)
                    {
                        entry.setWarFilter(filters[--counter]);
                        if (counter > 0)
                        {
                            entry.setEarFilter(filters[--counter]);
                            if (counter > 0)
                            {
                                entry.setJmodFilter(filters[--counter]);
                                if (counter > 0)
                                {
                                    entry.setZipFilter(filters[--counter]);
                                    if (counter > 0)
                                    {
                                        // For backward compatibility, the
                                        // jmod/aar/aab/apk filters come first
                                        // in the list.
                                        entry.setApkFilter(filters[--counter]);
                                        if (counter > 0)
                                        {
                                            entry.setAabFilter(filters[--counter]);
                                            if (counter > 0)
                                            {
                                                entry.setAarFilter(filters[--counter]);
                                                if (counter > 0)
                                                {
                                                    entry.setJmodFilter(filters[--counter]);
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                // Read the separator, if any.
                readNextWord();
            }

            // Add the entry to the list.
            classPath.add(entry);

            if (configurationEnd())
            {
                return classPath;
            }

            if (!nextWord.equals(ConfigurationConstants.JAR_SEPARATOR_KEYWORD))
            {
                throw new ParseException("Expecting class path separator '" + ConfigurationConstants.JAR_SEPARATOR_KEYWORD +
                                         "' before " + reader.locationDescription());
            }

            // Read the next jar name.
            readNextWord("jar or directory name", true, false, false);
        }
    }


    private int parseClassVersion()
    throws ParseException, IOException
    {
        // Read the obligatory target.
        readNextWord("java version");

        int classVersion = ClassUtil.internalClassVersion(nextWord);
        if (classVersion == 0)
        {
            throw new ParseException("Unsupported java version " + reader.locationDescription());
        }

        readNextWord();

        return classVersion;
    }


    private int parseIntegerArgument()
    throws ParseException, IOException
    {
        try
        {
            // Read the obligatory integer.
            readNextWord("integer");

            int integer = Integer.parseInt(nextWord);
            if (integer <= 0)
            {
                throw new ParseException("Expecting value larger than 0, instead of '" + nextWord +
                                         "' before " + reader.locationDescription());
            }

            readNextWord();

            return integer;
        }
        catch (NumberFormatException e)
        {
            throw new ParseException("Expecting integer argument instead of '" + nextWord +
                                     "' before " + reader.locationDescription());
        }
    }


    private URL parseURL()
    throws ParseException, IOException
    {
        // Read the obligatory file name.
        readNextWord("file name", true, true, false);

        // Make sure the file is properly resolved.
        URL url = url(nextWord);

        readNextWord();

        return url;
    }


    private List parseFiles(List files)
    throws ParseException, IOException
    {
        if (files == null)
        {
            files = new ArrayList();
        }

        files.add(parseFile());

        return files;
    }


    private File parseFile()
    throws ParseException, IOException
    {
        // Read the obligatory file name.
        readNextWord("file name", true, true, false);

        // Make sure the file is properly resolved.
        File file = file(nextWord);

        readNextWord();

        return file;
    }


    private File parseOptionalFile()
    throws ParseException, IOException
    {
        // Read the optional file name.
        readNextWord(true, true);

        // Didn't the user specify a file name?
        if (configurationEnd())
        {
            return Configuration.STD_OUT;
        }

        // Make sure the file is properly resolved.
        File file = file(nextWord);

        readNextWord();

        return file;
    }


    private String parseOptionalArgument() throws IOException
    {
        // Read the optional argument.
        readNextWord();

        // Didn't the user specify an argument?
        if (configurationEnd())
        {
            return "";
        }

        String argument = nextWord;

        readNextWord();

        return argument;
    }


    private boolean parseNoArgument(boolean value) throws IOException
    {
        readNextWord();

        return value;
    }


    private long parseNoArgument(long value) throws IOException
    {
        readNextWord();

        return value;
    }


    /**
     * Parses and adds a conditional class specification to keep other classes
     * and class members.
     * For example: -if "public class SomeClass { void someMethod(); } -keep"
     * @throws ParseException if the class specification contains a syntax error.
     * @throws IOException    if an IO error occurs while reading the class
     *                        specification.
     */
    private List parseIfCondition(List keepClassSpecifications)
    throws ParseException, IOException
    {
        // Read the condition.
        ClassSpecification condition =
            parseClassSpecificationArguments(true, true, false);

        // Read the corresponding keep option.
        if (nextWord == null)
        {
            throw new ParseException("Expecting '-keep' option after '-if' option, before " + reader.locationDescription());
        }

        if      (ConfigurationConstants.KEEP_OPTION                          .startsWith(nextWord)) keepClassSpecifications = parseKeepClassSpecificationArguments(keepClassSpecifications, true,  true,  false, false, false, condition);
        else if (ConfigurationConstants.KEEP_CLASS_MEMBERS_OPTION            .startsWith(nextWord)) keepClassSpecifications = parseKeepClassSpecificationArguments(keepClassSpecifications, false, true,  false, false, false, condition);
        else if (ConfigurationConstants.KEEP_CLASSES_WITH_MEMBERS_OPTION     .startsWith(nextWord)) keepClassSpecifications = parseKeepClassSpecificationArguments(keepClassSpecifications, false, true,  false, true,  false, condition);
        else if (ConfigurationConstants.KEEP_NAMES_OPTION                    .startsWith(nextWord)) keepClassSpecifications = parseKeepClassSpecificationArguments(keepClassSpecifications, true,  true,  false, false, true,  condition);
        else if (ConfigurationConstants.KEEP_CLASS_MEMBER_NAMES_OPTION       .startsWith(nextWord)) keepClassSpecifications = parseKeepClassSpecificationArguments(keepClassSpecifications, false, true,  false, false, true,  condition);
        else if (ConfigurationConstants.KEEP_CLASSES_WITH_MEMBER_NAMES_OPTION.startsWith(nextWord)) keepClassSpecifications = parseKeepClassSpecificationArguments(keepClassSpecifications, false, true,  false, true,  true,  condition);
        else if (ConfigurationConstants.KEEP_CODE_OPTION                     .startsWith(nextWord)) keepClassSpecifications = parseKeepClassSpecificationArguments(keepClassSpecifications, false, false, true,  false, false, condition);
        else
        {
            throw new ParseException("Expecting '-keep' option after '-if' option, before " + reader.locationDescription());
        }

        return keepClassSpecifications;
    }


    /**
     * Parses and adds a class specification to keep classes and class members.
     * For example: -keep "public class SomeClass { void someMethod(); }"
     * @throws ParseException if the class specification contains a syntax error.
     * @throws IOException    if an IO error occurs while reading the class
     *                        specification.
     */
    private List parseKeepClassSpecificationArguments(List               keepClassSpecifications,
                                                      boolean            markClasses,
                                                      boolean            markMembers,
                                                      boolean            markCodeAttributes,
                                                      boolean            markConditionally,
                                                      boolean            allowShrinking,
                                                      ClassSpecification condition)
    throws ParseException, IOException
    {
        // Create a new List if necessary.
        if (keepClassSpecifications == null)
        {
            keepClassSpecifications = new ArrayList();
        }

        // Read and add the keep configuration.
        keepClassSpecifications.add(parseKeepClassSpecificationArguments(markClasses,
                                                                         markMembers,
                                                                         markCodeAttributes,
                                                                         markConditionally,
                                                                         allowShrinking,
                                                                         condition));
        return keepClassSpecifications;
    }


    /**
     * Parses and returns a class specification to keep classes and class
     * members.
     * For example: -keep "public class SomeClass { void someMethod(); }"
     * @throws ParseException if the class specification contains a syntax error.
     * @throws IOException    if an IO error occurs while reading the class
     *                        specification.
     */
    private KeepClassSpecification parseKeepClassSpecificationArguments(boolean            markClasses,
                                                                        boolean            markMembers,
                                                                        boolean            markCodeAttributes,
                                                                        boolean            markConditionally,
                                                                        boolean            allowShrinking,
                                                                        ClassSpecification condition)
    throws ParseException, IOException
    {
        boolean markDescriptorClasses = false;
        //boolean allowShrinking        = false;
        boolean allowOptimization     = false;
        boolean allowObfuscation      = false;

        // Read the keep modifiers.
        while (true)
        {
            readNextWord("keyword '" + ConfigurationConstants.CLASS_KEYWORD +
                         "', '"      + JavaAccessConstants.INTERFACE +
                         "', or '"   + JavaAccessConstants.ENUM + "'",
                         false, false, true);

            if (!ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD.equals(nextWord))
            {
                // Not a comma. Stop parsing the keep modifiers.
                break;
            }

            readNextWord("keyword '" + ConfigurationConstants.ALLOW_SHRINKING_SUBOPTION +
                         "', '"      + ConfigurationConstants.ALLOW_OPTIMIZATION_SUBOPTION +
                         "', or '"   + ConfigurationConstants.ALLOW_OBFUSCATION_SUBOPTION + "'");

            if      (ConfigurationConstants.INCLUDE_DESCRIPTOR_CLASSES_SUBOPTION.startsWith(nextWord))
            {
                markDescriptorClasses = true;
            }
            else if (ConfigurationConstants.INCLUDE_CODE_SUBOPTION              .startsWith(nextWord))
            {
                markCodeAttributes    = true;
            }
            else if (ConfigurationConstants.ALLOW_SHRINKING_SUBOPTION           .startsWith(nextWord))
            {
                allowShrinking        = true;
            }
            else if (ConfigurationConstants.ALLOW_OPTIMIZATION_SUBOPTION        .startsWith(nextWord))
            {
                allowOptimization     = true;
            }
            else if (ConfigurationConstants.ALLOW_OBFUSCATION_SUBOPTION         .startsWith(nextWord))
            {
                allowObfuscation      = true;
            }
            else
            {
                throw new ParseException("Expecting keyword '" + ConfigurationConstants.INCLUDE_DESCRIPTOR_CLASSES_SUBOPTION +
                                         "', '"                + ConfigurationConstants.INCLUDE_CODE_SUBOPTION +
                                         "', '"                + ConfigurationConstants.ALLOW_SHRINKING_SUBOPTION +
                                         "', '"                + ConfigurationConstants.ALLOW_OPTIMIZATION_SUBOPTION +
                                         "', or '"             + ConfigurationConstants.ALLOW_OBFUSCATION_SUBOPTION +
                                         "' before " + reader.locationDescription());
            }
        }

        // Read the class configuration.
        ClassSpecification classSpecification =
            parseClassSpecificationArguments(false, true, false);

        // Create and return the keep configuration.
        return new KeepClassSpecification(markClasses,
                                          markMembers,
                                          markConditionally,
                                          markDescriptorClasses,
                                          markCodeAttributes,
                                          allowShrinking,
                                          allowOptimization,
                                          allowObfuscation,
                                          condition,
                                          classSpecification);
    }


    /**
     * Parses and adds a class specification for optimization assumptions.
     * For example: -assumenosideeffects "public class Simple { void simple(); }"
     * @throws ParseException if the class specification contains a syntax error.
     * @throws IOException    if an IO error occurs while reading the class
     *                        specification.
     */
    private List parseAssumeClassSpecificationArguments(List classSpecifications)
    throws ParseException, IOException
    {
        // Create a new List if necessary.
        if (classSpecifications == null)
        {
            classSpecifications = new ArrayList();
        }

        // Read and add the class configuration.
        classSpecifications.add(parseClassSpecificationArguments(true, true, true));

        return classSpecifications;
    }


    /**
     * Parses and adds a class specification.
     * For example: "class SomeClass { void someMethod(); }"
     * @throws ParseException if the class specification contains a syntax error.
     * @throws IOException    if an IO error occurs while reading the class
     *                        specification.
     */
    private List parseClassSpecificationArguments(List classSpecifications)
    throws ParseException, IOException
    {
        // Create a new List if necessary.
        if (classSpecifications == null)
        {
            classSpecifications = new ArrayList();
        }

        // Read and add the keep configuration.
        classSpecifications.add(parseClassSpecificationArguments(true, true, false));

        return classSpecifications;
    }


    // Added for compatibility with older versions of ProGuard (see PGD-755).
    public ClassSpecification parseClassSpecificationArguments()
    throws ParseException, IOException
    {
        return parseClassSpecificationArguments(false, true, false);
    }


    /**
     * Parses and returns a class specification.
     * For example: "public class SomeClass { public void someMethod(); }"
     * @throws ParseException if the class specification contains a syntax error.
     * @throws IOException    if an IO error occurs while reading the class
     *                        specification.
     */
    public ClassSpecification parseClassSpecificationArguments(boolean readFirstWord,
                                                               boolean allowClassMembers,
                                                               boolean allowValues)
    throws ParseException, IOException
    {
        if (readFirstWord)
        {
            readNextWord("keyword '" + ConfigurationConstants.CLASS_KEYWORD +
                         "', '"      + JavaAccessConstants.INTERFACE +
                         "', or '"   + JavaAccessConstants.ENUM + "'",
                         false, false, true);
        }

        // Clear the annotation type.
        String annotationType = null;

        // Clear the class access modifiers.
        int requiredSetClassAccessFlags   = 0;
        int requiredUnsetClassAccessFlags = 0;

        // Parse the class annotations and access modifiers until the class keyword.
        while (!ConfigurationConstants.CLASS_KEYWORD.equals(nextWord))
        {
            // Strip the negating sign, if any.
            boolean negated =
                nextWord.startsWith(ConfigurationConstants.NEGATOR_KEYWORD);

            String strippedWord = negated ?
                nextWord.substring(1) :
                nextWord;

            // Parse the class access modifiers.
            int accessFlag =
                strippedWord.equals(JavaAccessConstants.PUBLIC)     ? AccessConstants.PUBLIC      :
                strippedWord.equals(JavaAccessConstants.FINAL)      ? AccessConstants.FINAL       :
                strippedWord.equals(JavaAccessConstants.INTERFACE)  ? AccessConstants.INTERFACE   :
                strippedWord.equals(JavaAccessConstants.ABSTRACT)   ? AccessConstants.ABSTRACT    :
                strippedWord.equals(JavaAccessConstants.SYNTHETIC)  ? AccessConstants.SYNTHETIC   :
                strippedWord.equals(JavaAccessConstants.ANNOTATION) ? AccessConstants.ANNOTATION  :
                strippedWord.equals(JavaAccessConstants.ENUM)       ? AccessConstants.ENUM        :
                                                                      unknownAccessFlag();

            // Is it an annotation modifier?
            if (accessFlag == AccessConstants.ANNOTATION)
            {
                readNextWord("annotation type or keyword '" + JavaAccessConstants.INTERFACE + "'",
                             false, false, false);

                // Is the next word actually an annotation type?
                if (!nextWord.equals(JavaAccessConstants.INTERFACE) &&
                    !nextWord.equals(JavaAccessConstants.ENUM)      &&
                    !nextWord.equals(ConfigurationConstants.CLASS_KEYWORD))
                {
                    // Parse the annotation type.
                    annotationType =
                        ListUtil.commaSeparatedString(
                        parseCommaSeparatedList("annotation type",
                                                false, false, false, false, true, false, false, false, true, null), false);

                    // Continue parsing the access modifier that we just read
                    // in the next cycle.
                    continue;
                }

                // Otherwise just handle the annotation modifier.
            }

            if (!negated)
            {
                requiredSetClassAccessFlags   |= accessFlag;
            }
            else
            {
                requiredUnsetClassAccessFlags |= accessFlag;
            }

            if ((requiredSetClassAccessFlags &
                 requiredUnsetClassAccessFlags) != 0)
            {
                throw new ParseException("Conflicting class access modifiers for '" + strippedWord +
                                         "' before " + reader.locationDescription());
            }

            if (strippedWord.equals(JavaAccessConstants.INTERFACE) ||
                strippedWord.equals(JavaAccessConstants.ENUM)      ||
                strippedWord.equals(ConfigurationConstants.CLASS_KEYWORD))
            {
                // The interface or enum keyword. Stop parsing the class flags.
                break;
            }

            // Should we read the next word?
            if (accessFlag != AccessConstants.ANNOTATION)
            {
                readNextWord("keyword '" + ConfigurationConstants.CLASS_KEYWORD +
                             "', '"      + JavaAccessConstants.INTERFACE +
                             "', or '"   + JavaAccessConstants.ENUM + "'",
                             false, false, true);
            }
        }

       // Parse the class name part.
        String externalClassName =
            ListUtil.commaSeparatedString(
            parseCommaSeparatedList("class name or interface name",
                                    true, false, false, false, true, false, false, false, false, null), false);

        // For backward compatibility, allow a single "*" wildcard to match any
        // class.
        String className = ConfigurationConstants.ANY_CLASS_KEYWORD.equals(externalClassName) ?
            null :
            ClassUtil.internalClassName(externalClassName);

        // Clear the annotation type and the class name of the extends part.
        String extendsAnnotationType = null;
        String extendsClassName      = null;

        if (allowClassMembers && !configurationEnd())
        {
            // Parse 'implements ...' or 'extends ...' part, if any.
            if (ConfigurationConstants.IMPLEMENTS_KEYWORD.equals(nextWord) ||
                ConfigurationConstants.EXTENDS_KEYWORD.equals(nextWord))
            {
                readNextWord("class name or interface name", false, false, true);

                // Parse the annotation type, if any.
                if (ConfigurationConstants.ANNOTATION_KEYWORD.equals(nextWord))
                {
                    extendsAnnotationType =
                        ListUtil.commaSeparatedString(
                        parseCommaSeparatedList("annotation type",
                                                true, false, false, false, true, false, false, false, true, null), false);
                }

                String externalExtendsClassName =
                    ListUtil.commaSeparatedString(
                    parseCommaSeparatedList("class name or interface name",
                                            false, false, false, false, true, false, false, false, false, null), false);

                extendsClassName = ConfigurationConstants.ANY_CLASS_KEYWORD.equals(externalExtendsClassName) ?
                    null :
                    ClassUtil.internalClassName(externalExtendsClassName);
            }
        }

        // Create the basic class specification.
        ClassSpecification classSpecification =
            new ClassSpecification(lastComments,
                                   requiredSetClassAccessFlags,
                                   requiredUnsetClassAccessFlags,
                                   annotationType,
                                   className,
                                   extendsAnnotationType,
                                   extendsClassName);


        // Now add any class members to this class specification.
        if (allowClassMembers && !configurationEnd())
        {
            // Check the class member opening part.
            if (!ConfigurationConstants.OPEN_KEYWORD.equals(nextWord))
            {
                throw new ParseException("Expecting opening '" + ConfigurationConstants.OPEN_KEYWORD +
                                         "' at " + reader.locationDescription());
            }

            // Parse all class members.
            while (true)
            {
                readNextWord("class member description" +
                             " or closing '" + ConfigurationConstants.CLOSE_KEYWORD + "'",
                             false, false, true);

                if (nextWord.equals(ConfigurationConstants.CLOSE_KEYWORD))
                {
                    lastComments = reader.lastComments();
                    classSpecification.memberComments = lastComments;

                    // The closing brace. Stop parsing the class members.
                    readNextWord();

                    break;
                }

                parseMemberSpecificationArguments(externalClassName,
                                                  allowValues,
                                                  classSpecification);
            }
        }

        return classSpecification;
    }


    /**
     * Parses and adds a class member specification.
     * @throws ParseException if the class specification contains a syntax error.
     * @throws IOException    if an IO error occurs while reading the class
     *                        specification.
     */
    private void parseMemberSpecificationArguments(String             externalClassName,
                                                   boolean            allowValues,
                                                   ClassSpecification classSpecification)
    throws ParseException, IOException
    {
        // Clear the annotation name.
        String annotationType = null;

        // Parse the class member access modifiers, if any.
        int requiredSetMemberAccessFlags   = 0;
        int requiredUnsetMemberAccessFlags = 0;

        while (!configurationEnd(true))
        {
            // Parse the annotation type, if any.
            if (ConfigurationConstants.ANNOTATION_KEYWORD.equals(nextWord))
            {
                annotationType =
                    ListUtil.commaSeparatedString(
                    parseCommaSeparatedList("annotation type",
                                            true, false, false, false, true, false, false, false, true, null), false);
                continue;
            }

            String strippedWord = nextWord.startsWith("!") ?
                nextWord.substring(1) :
                nextWord;

            // Parse the class member access modifiers.
            int accessFlag =
                strippedWord.equals(JavaAccessConstants.PUBLIC)       ? AccessConstants.PUBLIC       :
                strippedWord.equals(JavaAccessConstants.PRIVATE)      ? AccessConstants.PRIVATE      :
                strippedWord.equals(JavaAccessConstants.PROTECTED)    ? AccessConstants.PROTECTED    :
                strippedWord.equals(JavaAccessConstants.STATIC)       ? AccessConstants.STATIC       :
                strippedWord.equals(JavaAccessConstants.FINAL)        ? AccessConstants.FINAL        :
                strippedWord.equals(JavaAccessConstants.SYNCHRONIZED) ? AccessConstants.SYNCHRONIZED :
                strippedWord.equals(JavaAccessConstants.VOLATILE)     ? AccessConstants.VOLATILE     :
                strippedWord.equals(JavaAccessConstants.TRANSIENT)    ? AccessConstants.TRANSIENT    :
                strippedWord.equals(JavaAccessConstants.BRIDGE)       ? AccessConstants.BRIDGE       :
                strippedWord.equals(JavaAccessConstants.VARARGS)      ? AccessConstants.VARARGS      :
                strippedWord.equals(JavaAccessConstants.NATIVE)       ? AccessConstants.NATIVE       :
                strippedWord.equals(JavaAccessConstants.ABSTRACT)     ? AccessConstants.ABSTRACT     :
                strippedWord.equals(JavaAccessConstants.STRICT)       ? AccessConstants.STRICT       :
                strippedWord.equals(JavaAccessConstants.SYNTHETIC)    ? AccessConstants.SYNTHETIC    :
                                                                        0;
            if (accessFlag == 0)
            {
                // Not a class member access modifier. Stop parsing them.
                break;
            }

            if (strippedWord.equals(nextWord))
            {
                requiredSetMemberAccessFlags   |= accessFlag;
            }
            else
            {
                requiredUnsetMemberAccessFlags |= accessFlag;
            }

            // Make sure the user doesn't try to set and unset the same
            // access flags simultaneously.
            if ((requiredSetMemberAccessFlags &
                 requiredUnsetMemberAccessFlags) != 0)
            {
                throw new ParseException("Conflicting class member access modifiers for " +
                                         reader.locationDescription());
            }

            readNextWord("class member description");
        }

        // Parse the class member type and name part.

        // Did we get a special wildcard?
        if (ConfigurationConstants.ANY_CLASS_MEMBER_KEYWORD.equals(nextWord) ||
            ConfigurationConstants.ANY_FIELD_KEYWORD       .equals(nextWord) ||
            ConfigurationConstants.ANY_METHOD_KEYWORD      .equals(nextWord))
        {
            // Act according to the type of wildcard.
            if (ConfigurationConstants.ANY_CLASS_MEMBER_KEYWORD.equals(nextWord))
            {
                checkFieldAccessFlags(requiredSetMemberAccessFlags,
                                      requiredUnsetMemberAccessFlags);
                checkMethodAccessFlags(requiredSetMemberAccessFlags,
                                       requiredUnsetMemberAccessFlags);

                classSpecification.addField(
                    new MemberSpecification(requiredSetMemberAccessFlags,
                                            requiredUnsetMemberAccessFlags,
                                            annotationType,
                                            null,
                                            null));
                classSpecification.addMethod(
                    new MemberSpecification(requiredSetMemberAccessFlags,
                                            requiredUnsetMemberAccessFlags,
                                            annotationType,
                                            null,
                                            null));
            }
            else if (ConfigurationConstants.ANY_FIELD_KEYWORD.equals(nextWord))
            {
                checkFieldAccessFlags(requiredSetMemberAccessFlags,
                                      requiredUnsetMemberAccessFlags);

                classSpecification.addField(
                    new MemberSpecification(requiredSetMemberAccessFlags,
                                            requiredUnsetMemberAccessFlags,
                                            annotationType,
                                            null,
                                            null));
            }
            else if (ConfigurationConstants.ANY_METHOD_KEYWORD.equals(nextWord))
            {
                checkMethodAccessFlags(requiredSetMemberAccessFlags,
                                       requiredUnsetMemberAccessFlags);

                classSpecification.addMethod(
                    new MemberSpecification(requiredSetMemberAccessFlags,
                                            requiredUnsetMemberAccessFlags,
                                            annotationType,
                                            null,
                                            null));
            }

            // We still have to read the closing separator.
            readNextWord("separator '" + ConfigurationConstants.SEPARATOR_KEYWORD + "'");

            if (!ConfigurationConstants.SEPARATOR_KEYWORD.equals(nextWord))
            {
                throw new ParseException("Expecting separator '" + ConfigurationConstants.SEPARATOR_KEYWORD +
                                         "' before " + reader.locationDescription());
            }
        }
        else
        {
            // Make sure we have a proper type.
            checkJavaIdentifier("java type");
            String type         = nextWord;
            String typeLocation = reader.locationDescription();

            readNextWord("class member name");
            String name = nextWord;

            // Did we get just one word before the opening parenthesis?
            if (ConfigurationConstants.OPEN_ARGUMENTS_KEYWORD.equals(name))
            {
                // This must be a constructor then.
                // Make sure the type is a proper constructor name.
                if (!(type.equals(ClassConstants.METHOD_NAME_INIT) ||
                      type.equals(externalClassName) ||
                      type.equals(ClassUtil.externalShortClassName(externalClassName))))
                {
                    throw new ParseException("Expecting type and name " +
                                             "instead of just '" + type +
                                             "' before " + reader.locationDescription());
                }

                // Assign the fixed constructor type and name.
                type = JavaTypeConstants.VOID;
                name = ClassConstants.METHOD_NAME_INIT;
            }
            else
            {
                // It's not a constructor.
                // Make sure we have a proper name.
                checkJavaIdentifier("class member name");

                // Read the opening parenthesis or the separating
                // semi-colon.
                readNextWord("opening '" + ConfigurationConstants.OPEN_ARGUMENTS_KEYWORD +
                             "' or separator '" + ConfigurationConstants.SEPARATOR_KEYWORD + "'");
            }

            // Check if the type actually contains the use of generics.
            // Can not do it right away as we also support "" as a type (see case above).
            if (containsGenerics(type))
            {
                throw new ParseException("Generics are not allowed (erased) for java type" + typeLocation);
            }

            // Are we looking at a field, a method, or something else?
            if (ConfigurationConstants.SEPARATOR_KEYWORD.equals(nextWord))
            {
                // It's a field.
                checkFieldAccessFlags(requiredSetMemberAccessFlags,
                                      requiredUnsetMemberAccessFlags);

                // We already have a field descriptor.
                String descriptor = ClassUtil.internalType(type);

                if (ConfigurationConstants.ANY_FIELD_KEYWORD.equals(name))
                {
                    throw new ParseException("Not expecting field type before with wildcard '" + ConfigurationConstants.ANY_FIELD_KEYWORD +
                                             "before " + reader.locationDescription() +
                                             " (use '" + ConfigurationConstants.ANY_CLASS_MEMBER_KEYWORD + "' instead)");
                }

                // Add the field.
                classSpecification.addField(
                    new MemberSpecification(requiredSetMemberAccessFlags,
                                            requiredUnsetMemberAccessFlags,
                                            annotationType,
                                            name,
                                            descriptor));
            }
            else if (allowValues &&
                     (ConfigurationConstants.EQUAL_KEYWORD.equals(nextWord) ||
                      ConfigurationConstants.RETURN_KEYWORD.equals(nextWord)))
            {
                // It's a field with a value.
                checkFieldAccessFlags(requiredSetMemberAccessFlags,
                                      requiredUnsetMemberAccessFlags);

                // We already have a field descriptor.
                String descriptor = ClassUtil.internalType(type);

                // Read the constant.
                Number[] values = parseValues(type, descriptor);

                // Read the separator after the constant.
                readNextWord("separator '" + ConfigurationConstants.SEPARATOR_KEYWORD + "'");

                if (!ConfigurationConstants.SEPARATOR_KEYWORD.equals(nextWord))
                {
                    throw new ParseException("Expecting separator '" + ConfigurationConstants.SEPARATOR_KEYWORD +
                                             "' before " + reader.locationDescription());
                }

                // Add the field.
                classSpecification.addField(
                    new MemberValueSpecification(requiredSetMemberAccessFlags,
                                                 requiredUnsetMemberAccessFlags,
                                                 annotationType,
                                                 name,
                                                 descriptor,
                                                 values));
            }
            else if (ConfigurationConstants.OPEN_ARGUMENTS_KEYWORD.equals(nextWord))
            {
                // It's a method.
                checkMethodAccessFlags(requiredSetMemberAccessFlags,
                                       requiredUnsetMemberAccessFlags);

                // Parse the method arguments.
                String descriptor =
                    ClassUtil.internalMethodDescriptor(type,
                                                       parseCommaSeparatedList("argument", true, true, true, false, true, false, false, false, false, null));

                if (!ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD.equals(nextWord))
                {
                    throw new ParseException("Expecting separating '" + ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD +
                                             "' or closing '" + ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD +
                                             "' before " + reader.locationDescription());
                }

                // Read the separator after the closing parenthesis.
                readNextWord("separator '" + ConfigurationConstants.SEPARATOR_KEYWORD + "'");

                if (ConfigurationConstants.SEPARATOR_KEYWORD.equals(nextWord))
                {
                    if (ConfigurationConstants.ANY_METHOD_KEYWORD.equals(name))
                    {
                        throw new ParseException("Not expecting method descriptor with wildcard '" + ConfigurationConstants.ANY_METHOD_KEYWORD +
                                                 "before " + reader.locationDescription() +
                                                 " (use '" + ConfigurationConstants.ANY_CLASS_MEMBER_KEYWORD + "' instead)");
                    }

                    // Add the plain method.
                    classSpecification.addMethod(
                        new MemberSpecification(requiredSetMemberAccessFlags,
                                                requiredUnsetMemberAccessFlags,
                                                annotationType,
                                                name,
                                                descriptor));
                }
                else if (allowValues &&
                         (ConfigurationConstants.EQUAL_KEYWORD.equals(nextWord) ||
                          ConfigurationConstants.RETURN_KEYWORD.equals(nextWord)))
                {
                    // It's a method with a value.
                    checkFieldAccessFlags(requiredSetMemberAccessFlags,
                                          requiredUnsetMemberAccessFlags);

                    // Read the constant.
                    Number[] values = parseValues(type, ClassUtil.internalType(type));

                    // Read the separator after the constant.
                    readNextWord("separator '" + ConfigurationConstants.SEPARATOR_KEYWORD + "'");

                    if (!ConfigurationConstants.SEPARATOR_KEYWORD.equals(nextWord))
                    {
                        throw new ParseException("Expecting separator '" + ConfigurationConstants.SEPARATOR_KEYWORD +
                                                 "' before " + reader.locationDescription());
                    }

                    // Add the method.
                    classSpecification.addMethod(
                        new MemberValueSpecification(requiredSetMemberAccessFlags,
                                                     requiredUnsetMemberAccessFlags,
                                                     annotationType,
                                                     name,
                                                     descriptor,
                                                     values));
                }
                else
                {
                    throw new ParseException("Expecting separator '" + ConfigurationConstants.SEPARATOR_KEYWORD +
                                             "' before " + reader.locationDescription());

                }
            }
            else
            {
                // It doesn't look like a field or a method.
                throw new ParseException("Expecting opening '" + ConfigurationConstants.OPEN_ARGUMENTS_KEYWORD +
                                         "' or separator '" + ConfigurationConstants.SEPARATOR_KEYWORD +
                                         "' before " + reader.locationDescription());
            }
        }
    }


    /**
     * Reads a value or value range of the given primitive type.
     * For example, values "123" or "100..199" of type "int" ("I").
     */
    private Number[] parseValues(String externalType,
                                 String internalType)
    throws ParseException, IOException
    {
        readNextWord(externalType + " constant");

        int rangeIndex = nextWord.indexOf(ConfigurationConstants.RANGE_KEYWORD);
        return rangeIndex >= 0 ?
            new Number[]
            {
                parseValue(externalType, internalType, nextWord.substring(0, rangeIndex)),
                parseValue(externalType, internalType, nextWord.substring(rangeIndex + ConfigurationConstants.RANGE_KEYWORD.length()))
            } :
            new Number[]
            {
                parseValue(externalType, internalType, nextWord)
            };
    }


    /**
     * Parses the given string as a value of the given primitive type.
     * For example, value "123" of type "int" ("I").
     * For example, value "true" of type "boolean" ("Z"), returned as 1.
     */
    private Number parseValue(String externalType,
                              String internalType,
                              String string)
    throws ParseException
    {
        try
        {
            string = replaceSystemProperties(string);

            switch (internalType.charAt(0))
            {
                case TypeConstants.BOOLEAN:
                {
                    return parseBoolean(string);
                }
                case TypeConstants.BYTE:
                case TypeConstants.CHAR:
                case TypeConstants.SHORT:
                case TypeConstants.INT:
                {
                    return Integer.decode(string);
                }
                //case TypeConstants.LONG:
                //{
                //    return Long.decode(string);
                //}
                //case TypeConstants.FLOAT:
                //{
                //    return Float.valueOf(string);
                //}
                //case TypeConstants.DOUBLE:
                //{
                //    return Double.valueOf(string);
                //}
                default:
                {
                    throw new ParseException("Can't handle '" + externalType +
                                             "' constant " + reader.locationDescription());
                }
            }
        }
        catch (NumberFormatException e)
        {
            throw new ParseException("Can't parse " + externalType +
                                     " constant " + reader.locationDescription());
        }
    }


    /**
     * Parses the given boolean string as an integer (0 or 1).
     */
    private Integer parseBoolean(String string)
    throws ParseException
    {
        if      (ConfigurationConstants.FALSE_KEYWORD.equals(nextWord))
        {
            return Integer.valueOf(0);
        }
        else if (ConfigurationConstants.TRUE_KEYWORD.equals(nextWord))
        {
            return Integer.valueOf(1);
        }
        else
        {
            throw new ParseException("Unknown boolean constant " + reader.locationDescription());
        }
    }


    /**
     * Reads a comma-separated list of Lists of java identifiers or of file
     * names.
     */
    private List parseCommaSeparatedLists(String  expectedDescription,
                                          boolean readFirstWord,
                                          boolean allowEmptyList,
                                          boolean expectClosingParenthesis,
                                          boolean isFileName,
                                          boolean checkJavaIdentifiers,
                                          boolean allowGenerics,
                                          boolean replaceSystemProperties,
                                          boolean replaceExternalClassNames,
                                          boolean replaceExternalTypes,
                                          List    list)
    throws ParseException, IOException
    {
        if (list == null)
        {
            list = new ArrayList();
        }

        // Parse a new list and add it to our list.
        list.add(parseCommaSeparatedList(expectedDescription,
                                         readFirstWord,
                                         allowEmptyList,
                                         expectClosingParenthesis,
                                         isFileName,
                                         checkJavaIdentifiers,
                                         allowGenerics,
                                         replaceSystemProperties,
                                         replaceExternalClassNames,
                                         replaceExternalTypes,
                                         null));
        return list;
    }


    /**
     * Reads a comma-separated list of java identifiers or of file names.
     * Examples of invocation arguments:
     *
     *   expected           read   allow  expect  is     check  allow   replace replace replace
     *   description        First  empty  Closing File   Java   Generic System  Extern  Extern
     *                      Word   List   Paren   Name   Id             Prop    Class   Types
     *   ----------------------------------------------------------------------------------
     *   ("directory name", true,  true,  false,  true,  false, true,   true,   false,  false, ...)
     *   ("optimization",   true,  false, false,  false, false, true,   false,  false,  false, ...)
     *   ("package name",   true,  true,  false,  false, true,  false,  false,  true,   false, ...)
     *   ("attribute name", true,  true,  false,  false, true,  false,  false,  false,  false, ...)
     *   ("class name",     true,  true,  false,  false, true,  false,  false,  true,   false, ...)
     *   ("filter",         true,  true,  true,   true,  false, true,   true,   false,  false, ...)
     *   ("annotation ",    false, false, false,  false, true,  false,  false,  false,  true,  ...)
     *   ("class name ",    true,  false, false,  false, true,  false,  false,  false,  false, ...)
     *   ("annotation ",    true,  false, false,  false, true,  false,  false,  false,  true,  ...)
     *   ("class name ",    false, false, false,  false, true,  false,  false,  false,  false, ...)
     *   ("annotation ",    true,  false, false,  false, true,  false,  false,  false,  true,  ...)
     *   ("argument",       true,  true,  true,   false, true,  false,  false,  false,  false, ...)
     */
    private List parseCommaSeparatedList(String  expectedDescription,
                                         boolean readFirstWord,
                                         boolean allowEmptyList,
                                         boolean expectClosingParenthesis,
                                         boolean isFileName,
                                         boolean checkJavaIdentifiers,
                                         boolean allowGenerics,
                                         boolean replaceSystemProperties,
                                         boolean replaceExternalClassNames,
                                         boolean replaceExternalTypes,
                                         List    list)
    throws ParseException, IOException
    {
        return parseCommaSeparatedList(expectedDescription,
                                       readFirstWord,
                                       allowEmptyList,
                                       null,
                                       expectClosingParenthesis,
                                       isFileName,
                                       checkJavaIdentifiers,
                                       allowGenerics,
                                       replaceSystemProperties,
                                       replaceExternalClassNames,
                                       replaceExternalTypes,
                                       list);
    }


    private List parseCommaSeparatedList(String  expectedDescription,
                                         boolean readFirstWord,
                                         boolean allowEmptyList,
                                         String  defaultIfEmpty,
                                         boolean expectClosingParenthesis,
                                         boolean isFileName,
                                         boolean checkJavaIdentifiers,
                                         boolean allowGenerics,
                                         boolean replaceSystemProperties,
                                         boolean replaceExternalClassNames,
                                         boolean replaceExternalTypes,
                                         List    list)
    throws ParseException, IOException
    {
        if (list == null)
        {
            list = new ArrayList();
        }

        if (readFirstWord)
        {
            if (!allowEmptyList)
            {
                // Read the first list entry.
                readNextWord(expectedDescription, isFileName, true, false);
            }
            else if (expectClosingParenthesis)
            {
                // Read the first list entry.
                readNextWord(expectedDescription, isFileName, true, false);

                // Return if the entry is actually empty (an empty file name or
                // a closing parenthesis).
                if (nextWord.length() == 0)
                {
                    // Read the closing parenthesis
                    readNextWord("closing '" + ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD +
                                 "'");

                    if (defaultIfEmpty != null)
                    {
                        list.add(defaultIfEmpty);
                    }
                    return list;
                }
                else if (nextWord.equals(ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD))
                {
                    if (defaultIfEmpty != null)
                    {
                        list.add(defaultIfEmpty);
                    }
                    return list;
                }
            }
            else
            {
                // Read the first list entry, if there is any.
                readNextWord(isFileName, true);

                // Check if the list is empty.
                if (configurationEnd())
                {
                    if (defaultIfEmpty != null)
                    {
                        list.add(defaultIfEmpty);
                    }
                    return list;
                }
            }
        }

        while (true)
        {
            if (checkJavaIdentifiers)
            {
                checkJavaIdentifier("java type", allowGenerics);
            }

            if (replaceSystemProperties)
            {
                nextWord = replaceSystemProperties(nextWord);
            }

            if (replaceExternalClassNames)
            {
                nextWord = ClassUtil.internalClassName(nextWord);
            }

            if (replaceExternalTypes)
            {
                nextWord = ClassUtil.internalType(nextWord);
            }

            list.add(nextWord);

            if (expectClosingParenthesis)
            {
                // Read a comma (or a closing parenthesis, or a different word).
                readNextWord("separating '" + ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD +
                             "' or closing '" + ConfigurationConstants.CLOSE_ARGUMENTS_KEYWORD +
                             "'");
            }
            else
            {
                // Read a comma (or a different word).
                readNextWord();
            }

            if (!ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD.equals(nextWord))
            {
                return list;
            }

            // Read the next list entry.
            readNextWord(expectedDescription, isFileName, true, false);
        }
    }


    /**
     * Converts a list of class name matchers to a class specification.
     */
    private ClassSpecification convertToClassSpecification(List list)
    {
        return new ClassSpecification(null,
                                      0,
                                      0,
                                      null,
                                      StringUtil.join(",", list.toArray(new String[list.size()])),
                                      null,
                                      null);
    }


    /**
     * Removes any present member specification from the provided
     * class specification.
     */
    private ClassSpecification removeMemberSpecification(ClassSpecification classSpecification)
    {
        classSpecification.fieldSpecifications  = null;
        classSpecification.methodSpecifications = null;

        return classSpecification;
    }


    /**
     * Throws a ParseException for an unexpected keyword.
     */
    private int unknownAccessFlag() throws ParseException
    {
        throw new ParseException("Unexpected keyword " + reader.locationDescription());
    }


    /**
     * Creates a properly resolved URL, based on the given word.
     */
    private URL url(String word) throws ParseException, MalformedURLException
    {
        String fileName = replaceSystemProperties(word);
        URL url;

        try
        {
            // Check if the file name is a valid URL.
            url = new URL(fileName);
            return url;
        }
        catch (MalformedURLException ex) {}

        // Is it relative to a URL or to a file?
        URL baseURL = reader.getBaseURL();
        if (baseURL != null)
        {
            url = new URL(baseURL, fileName);
        }
        else
        {
            // Is the file a valid resource URL?
            url = ConfigurationParser.class.getResource(fileName);
            if (url == null)
            {
                File file = new File(fileName);

                // Try to get an absolute file.
                if (!file.isAbsolute())
                {
                    file = new File(reader.getBaseDir(), fileName);
                }

                url = file.toURI().toURL();
            }
        }

        return url;
    }


    /**
     * Creates a properly resolved File, based on the given word.
     */
    private File file(String word) throws ParseException
    {
        String fileName = replaceSystemProperties(word);
        File   file     = new File(fileName);

        // Try to get an absolute file.
        if (!file.isAbsolute())
        {
            file = new File(reader.getBaseDir(), fileName);
        }

        return file;
    }


    /**
     * Replaces any properties in the given word by their values.
     * For instance, the substring "" is replaced by its value.
     */
    private String replaceSystemProperties(String word) throws ParseException
    {
        int fromIndex = 0;
        while (true)
        {
            fromIndex = word.indexOf(ConfigurationConstants.OPEN_SYSTEM_PROPERTY, fromIndex);
            if (fromIndex < 0)
            {
                break;
            }

            int toIndex = word.indexOf(ConfigurationConstants.CLOSE_SYSTEM_PROPERTY, fromIndex+1);
            if (toIndex < 0)
            {
                break;
            }

            String propertyName  = word.substring(fromIndex+1, toIndex);
            String propertyValue = properties.getProperty(propertyName);
            if (propertyValue == null)
            {
                try
                {
                    // Allow integer names, since they may be references
                    // to wildcards.
                    Integer.parseInt(propertyName);

                    fromIndex = toIndex + 1;
                    continue;
                }
                catch (NumberFormatException e)
                {
                    throw new ParseException("Value of system property '" + propertyName +
                                             "' is undefined in " + reader.locationDescription());
                }
            }

            word = word.substring(0, fromIndex) + propertyValue + word.substring(toIndex+1);

            fromIndex += propertyValue.length();
        }

        return word;
    }


    /**
     * Reads the next word of the configuration in the 'nextWord' field,
     * throwing an exception if there is no next word.
     */
    private void readNextWord(String expectedDescription)
    throws ParseException, IOException
    {
        readNextWord(expectedDescription, false, false, false);
    }


    /**
     * Reads the next word of the configuration in the 'nextWord' field,
     * throwing an exception if there is no next word.
     */
    private void readNextWord(String  expectedDescription,
                              boolean isFileName,
                              boolean expectSingleFile,
                              boolean expectingAtCharacter)
    throws ParseException, IOException
    {
        readNextWord(isFileName, expectSingleFile);
        if (configurationEnd(expectingAtCharacter))
        {
            throw new ParseException("Expecting " + expectedDescription +
                                     " before " + reader.locationDescription());
        }
    }


    /**
     * Reads the next word of the configuration in the 'nextWord' field.
     */
    private void readNextWord() throws IOException
    {
        readNextWord(false, false);
    }


    /**
     * Reads the next word of the configuration in the 'nextWord' field.
     */
    private void readNextWord(boolean isFileName,
                              boolean expectSingleFile) throws IOException
    {
        nextWord = reader.nextWord(isFileName, expectSingleFile);
    }


    /**
     * Returns whether the end of the configuration has been reached.
     */
    private boolean configurationEnd()
    {
        return configurationEnd(false);
    }


    /**
     * Returns whether the end of the configuration has been reached.
     */
    private boolean configurationEnd(boolean expectingAtCharacter)
    {
        return nextWord == null ||
               nextWord.startsWith(ConfigurationConstants.OPTION_PREFIX) ||
               (!expectingAtCharacter &&
                nextWord.equals(ConfigurationConstants.AT_DIRECTIVE));
    }


    /**
     * Checks whether the given word is a valid Java identifier and throws
     * a ParseException if it isn't. Wildcard characters are accepted.
     */
    private void checkJavaIdentifier(String expectedDescription)
        throws ParseException
    {
        checkJavaIdentifier(expectedDescription, true);
    }


    /**
     * Checks whether the given word is a valid Java identifier and throws
     * a ParseException if it isn't. Wildcard characters are accepted.
     */
    private void checkJavaIdentifier(String expectedDescription, boolean allowGenerics)
    throws ParseException
    {
        if (!isJavaIdentifier(nextWord))
        {
            throw new ParseException("Expecting " + expectedDescription +
                                     " before " + reader.locationDescription());
        }

        if (!allowGenerics && containsGenerics(nextWord))
        {
            throw new ParseException("Generics are not allowed (erased) in " + expectedDescription +
                                     " " + reader.locationDescription());
        }
    }


    /**
     * Returns whether the given word is a valid Java identifier.
     * Wildcard characters are accepted.
     */
    private boolean isJavaIdentifier(String word)
    {
        if (word.length() == 0)
        {
            return false;
        }

        for (int index = 0; index < word.length(); index++)
        {
            char c = word.charAt(index);
            if (!(Character.isJavaIdentifierPart(c) ||
                  c == '.' ||
                  c == '[' ||
                  c == ']' ||
                  c == '<' ||
                  c == '>' ||
                  c == '-' ||
                  c == '!' ||
                  c == '*' ||
                  c == '?' ||
                  c == '%'))
            {
                return false;
            }
        }

        return true;
    }


    /**
     * Returns whether the given word contains angle brackets around
     * a non-digit string.
     */
    private boolean containsGenerics(String word)
    {
        int index = 0;

        while (true)
        {
            // Can we find an opening angular bracket?
            int openIndex = word.indexOf(TypeConstants.GENERIC_START, index);
            if (openIndex < 0)
            {
                return false;
            }

            // Can we find a corresponding closing angular bracket?
            int closeIndex = word.indexOf(TypeConstants.GENERIC_END, openIndex + 1);
            if (closeIndex < 0)
            {
                return false;
            }

            try
            {
                // Is it just a reference to a wildcard?
                Integer.parseInt(word.substring(openIndex + 1, closeIndex));
            }
            catch (NumberFormatException e)
            {
                // It's not; it's really a generic type.
                return true;
            }

            index = closeIndex + 1;
        }
    }


    /**
     * Checks whether the given access flags are valid field access flags,
     * throwing a ParseException if they aren't.
     */
    private void checkFieldAccessFlags(int requiredSetMemberAccessFlags,
                                       int requiredUnsetMemberAccessFlags)
    throws ParseException
    {
        if (((requiredSetMemberAccessFlags |
              requiredUnsetMemberAccessFlags) &
            ~AccessConstants.VALID_FLAGS_FIELD) != 0)
        {
            throw new ParseException("Invalid method access modifier for field before " +
                                     reader.locationDescription());
        }
    }


    /**
     * Checks whether the given access flags are valid method access flags,
     * throwing a ParseException if they aren't.
     */
    private void checkMethodAccessFlags(int requiredSetMemberAccessFlags,
                                        int requiredUnsetMemberAccessFlags)
    throws ParseException
    {
        if (((requiredSetMemberAccessFlags |
              requiredUnsetMemberAccessFlags) &
            ~AccessConstants.VALID_FLAGS_METHOD) != 0)
        {
            throw new ParseException("Invalid field access modifier for method before " +
                                     reader.locationDescription());
        }
    }


    /**
     * A main method for testing configuration parsing.
     */
    public static void main(String[] args)
    {
        try
        {
            try (ConfigurationParser parser = new ConfigurationParser(args, System.getProperties()))
            {
                parser.parse(new Configuration());
            }
            catch (ParseException ex)
            {
                ex.printStackTrace();
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy