content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one or more
   *  contributor license agreements.  See the NOTICE file distributed with
   *  this work for additional information regarding copyright ownership.
   *  The ASF licenses this file to You under the Apache License, Version 2.0
   *  (the "License"); you may not use this file except in compliance with
   *  the License.  You may obtain a copy of the License at
   *
   *      https://www.apache.org/licenses/LICENSE-2.0
   *
   *  Unless required by applicable law or agreed to in writing, software
   *  distributed under the License is distributed on an "AS IS" BASIS,
   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   *  See the License for the specific language governing permissions and
   *  limitations under the License.
   *
   */
  
  package org.apache.tools.ant.taskdefs.modules;
  
  import java.io.File;
  import java.io.ByteArrayOutputStream;
  import java.io.PrintStream;
  import java.io.IOException;
  
  import java.nio.file.Files;
  
  import java.util.Collection;
  import java.util.List;
  import java.util.ArrayList;
  
  import java.util.Map;
  import java.util.LinkedHashMap;
  
  import java.util.Collections;
  
  import java.util.spi.ToolProvider;
  
  import org.apache.tools.ant.BuildException;
  import org.apache.tools.ant.Project;
  import org.apache.tools.ant.Task;
  
  import org.apache.tools.ant.util.MergingMapper;
  import org.apache.tools.ant.util.FileUtils;
  import org.apache.tools.ant.util.ResourceUtils;
  
  import org.apache.tools.ant.types.EnumeratedAttribute;
  import org.apache.tools.ant.types.FileSet;
  import org.apache.tools.ant.types.ModuleVersion;
  import org.apache.tools.ant.types.Path;
  import org.apache.tools.ant.types.Reference;
  import org.apache.tools.ant.types.Resource;
  import org.apache.tools.ant.types.ResourceCollection;
  
  import org.apache.tools.ant.types.resources.FileResource;
  import org.apache.tools.ant.types.resources.Union;
  
  /**
   * Creates a linkable .jmod file from a modular jar file, and optionally from
   * other resource files such as native libraries and documents.  Equivalent
   * to the JDK's
   * <a href="https://docs.oracle.com/en/java/javase/11/tools/jmod.html">jmod</a>
   * tool.
   * <p>
   * Supported attributes:
   * <dl>
   * <dt>{@code destFile}
   * <dd>Required, jmod file to create.
   * <dt>{@code classpath}
   * <dt>{@code classpathref}
   * <dd>Where to locate files to be placed in the jmod file.
   * <dt>{@code modulepath}
   * <dt>{@code modulepathref}
   * <dd>Where to locate dependencies.
   * <dt>{@code commandpath}
   * <dt>{@code commandpathref}
   * <dd>Directories containing native commands to include in jmod.
   * <dt>{@code headerpath}
   * <dt>{@code headerpathref}
   * <dd>Directories containing header files to include in jmod.
   * <dt>{@code configpath}
   * <dt>{@code configpathref}
   * <dd>Directories containing user-editable configuration files
   *     to include in jmod.
   * <dt>{@code legalpath}
   * <dt>{@code legalpathref}
   * <dd>Directories containing legal licenses and notices to include in jmod.
   * <dt>{@code nativelibpath}
   * <dt>{@code nativelibpathref}
   * <dd>Directories containing native libraries to include in jmod.
   * <dt>{@code manpath}
   * <dt>{@code manpathref}
   * <dd>Directories containing man pages to include in jmod.
   * <dt>{@code version}
   * <dd>Module <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/module/ModuleDescriptor.Version.html">version</a>.
   * <dt>{@code mainclass}
   * <dd>Main class of module.
   * <dt>{@code platform}
   * <dd>The target platform for the jmod.  A particular JDK's platform
   * can be seen by running
   * <code>jmod describe $JDK_HOME/jmods/java.base.jmod | grep -i platform</code>.
   * <dt>{@code hashModulesPattern}
   * <dd>Regular expression for names of modules in the module path
   *     which depend on the jmod being created, and which should have
   *     hashes generated for them and included in the new jmod.
   * <dt>{@code resolveByDefault}
   * <dd>Boolean indicating whether the jmod should be one of
   *     the default resolved modules in an application.  Default is true.
   * <dt>{@code moduleWarnings}
   * <dd>Whether to emit warnings when resolving modules which are
   *     not recommended for use.  Comma-separated list of one of more of
   *     the following:
   *     <dl>
   *     <dt>{@code deprecated}
   *     <dd>Warn if module is deprecated
   *     <dt>{@code leaving}
   *     <dd>Warn if module is deprecated for removal
   *     <dt>{@code incubating}
   *     <dd>Warn if module is an incubating (not yet official) module
   *     </dl>
   * </dl>
   *
   * <p>
   * Supported nested elements:
   * <dl>
   * <dt>{@code <classpath>}
   * <dd>Path indicating where to locate files to be placed in the jmod file.
   * <dt>{@code <modulepath>}
   * <dd>Path indicating where to locate dependencies.
   * <dt>{@code <commandpath>}
   * <dd>Path of directories containing native commands to include in jmod.
   * <dt>{@code <headerpath>}
   * <dd>Path of directories containing header files to include in jmod.
   * <dt>{@code <configpath>}
   * <dd>Path of directories containing user-editable configuration files
   *     to include in jmod.
   * <dt>{@code <legalpath>}
   * <dd>Path of directories containing legal notices to include in jmod.
   * <dt>{@code <nativelibpath>}
   * <dd>Path of directories containing native libraries to include in jmod.
   * <dt>{@code <manpath>}
   * <dd>Path of directories containing man pages to include in jmod.
   * <dt>{@code <version>}
   * <dd><a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/module/ModuleDescriptor.Version.html">Module version</a> of jmod.
   *     Must have a required {@code number} attribute.  May also have optional
   *     {@code preRelease} and {@code build} attributes.
   * <dt>{@code <moduleWarning>}
   * <dd>Has one required attribute, {@code reason}.  See {@code moduleWarnings}
   *     attribute above.  This element may be specified multiple times.
   * </dl>
   * <p>
   * destFile and classpath are required data.
   *
   * @since 1.10.6
   */
  public class Jmod
  extends Task {
      /** Location of jmod file to be created. */
      private File jmodFile;
  
      /**
       * Path of files (usually jar files or directories containing
       * compiled classes) from which to create jmod.
       */
      private Path classpath;
  
      /**
       * Path of directories containing modules on which the modules
       * in the classpath depend.
       */
      private Path modulePath;
  
      /**
       * Path of directories containing executable files to bundle in the
       * created jmod.
       */
      private Path commandPath;
  
      /**
       * Path of directories containing configuration files to bundle in the
       * created jmod.
       */
      private Path configPath;
  
      /**
       * Path of directories containing includable header files (such as for
       * other languages) to bundle in the created jmod.
       */
      private Path headerPath;
  
      /**
       * Path of directories containing legal license files to bundle
       * in the created jmod.
       */
      private Path legalPath;
  
      /**
       * Path of directories containing native libraries needed by classes
       * in the modules comprising the created jmod.
       */
      private Path nativeLibPath;
  
      /**
       * Path of directories containing manual pages to bundle
       * in the created jmod.
       */
      private Path manPath;
  
      /**
       * Module version of jmod.  Either this or {@link #moduleVersion}
       * may be set.
       */
      private String version;
  
      /** Module version of jmod.  Either this or {@link #version} may be set. */
      private ModuleVersion moduleVersion;
  
      /**
       * Main class to execute, if Java attempts to execute jmod's module
       * without specifying a main class explicitly.
       */
      private String mainClass;
  
      /**
       * Target platform of created jmod.  Examples are {@code windows-amd64}
       * and {@code linux-amd64}.  Target platform is an attribute
       * of each JDK, which can be seen by executing
       * <code>jmod describe $JDK_HOME/jmods/java.base.jmod</code> and
       * searching the output for a line starting with {@code platform}.
       */
      private String platform;
  
      /**
       * Regular expression matching names of modules which depend on the
       * the created jmod's module, for which hashes should be added to the
       * created jmod.
       */
      private String hashModulesPattern;
  
      /**
       * Whether the created jmod should be seen by Java when present in a
       * module path, even if not explicitly named.  Normally true.
       */
      private boolean resolveByDefault = true;
  
      /**
       * Reasons why module resolution during jmod creation may emit warnings.
       */
      private final List<ResolutionWarningSpec> moduleWarnings =
          new ArrayList<>();
  
      /**
       * Attribute containing the location of the jmod file to create.
       *
       * @return location of jmod file
       *
       * @see #setDestFile(File)
       */
      public File getDestFile() {
          return jmodFile;
      }
  
      /**
       * Sets attribute containing the location of the jmod file to create.
       * This value is required.
       *
       * @param file location where jmod file will be created.
       */
      public void setDestFile(final File file) {
          this.jmodFile = file;
      }
  
      /**
       * Adds an unconfigured {@code <classpath>} child element which can
       * specify the files which will comprise the created jmod.
       *
       * @return new, unconfigured child element
       *
       * @see #setClasspath(Path)
       */
      public Path createClasspath() {
          if (classpath == null) {
              classpath = new Path(getProject());
          }
          return classpath.createPath();
      }
  
      /**
       * Attribute which specifies the files (usually modular .jar files)
       * which will comprise the created jmod file.
       *
       * @return path of constituent files
       *
       * @see #setClasspath(Path)
       */
      public Path getClasspath() {
          return classpath;
      }
  
      /**
       * Sets attribute specifying the files that will comprise the created jmod
       * file.  Usually this contains a single modular .jar file.
       * <p>
       * The classpath is required and must not be empty.
       *
       * @param path path of files that will comprise jmod
       *
       * @see #createClasspath()
       */
      public void setClasspath(final Path path) {
          if (classpath == null) {
              this.classpath = path;
          } else {
              classpath.append(path);
          }
      }
  
      /**
       * Sets {@linkplain #setClasspath(Path) classpath attribute} from a
       * path reference.
       *
       * @param ref reference to path which will act as classpath
       */
      public void setClasspathRef(final Reference ref) {
          createClasspath().setRefid(ref);
      }
  
      /**
       * Creates a child {@code <modulePath>} element which can contain a
       * path of directories containing modules upon which modules in the
       * {@linkplain #setClasspath(Path) classpath} depend.
       *
       * @return new, unconfigured child element
       *
       * @see #setModulePath(Path)
       */
      public Path createModulePath() {
          if (modulePath == null) {
              modulePath = new Path(getProject());
          }
          return modulePath.createPath();
      }
  
      /**
       * Attribute containing path of directories which contain modules on which
       * the created jmod's {@linkplain #setClasspath(Path) constituent modules}
       * depend.
       *
       * @return path of directories containing modules needed by
       *         classpath modules
       *
       * @see #setModulePath(Path)
       */
      public Path getModulePath() {
          return modulePath;
      }
  
      /**
       * Sets attribute containing path of directories which contain modules
       * on which the created jmod's
       * {@linkplain #setClasspath(Path) constituent modules} depend.
       *
       * @param path path of directories containing modules needed by
       *             classpath modules
       *
       * @see #createModulePath()
       */
      public void setModulePath(final Path path) {
          if (modulePath == null) {
              this.modulePath = path;
          } else {
              modulePath.append(path);
          }
      }
  
      /**
       * Sets {@linkplain #setModulePath(Path) module path}
       * from a path reference.
       *
       * @param ref reference to path which will act as module path
       */
      public void setModulePathRef(final Reference ref) {
          createModulePath().setRefid(ref);
      }
  
      /**
       * Creates a child element which can contain a list of directories
       * containing native executable files to include in the created jmod.
       *
       * @return new, unconfigured child element
       *
       * @see #setCommandPath(Path)
       */
      public Path createCommandPath() {
          if (commandPath == null) {
              commandPath = new Path(getProject());
          }
          return commandPath.createPath();
      }
  
      /**
       * Attribute containing path of directories which contain native
       * executable files to include in the created jmod.
       *
       * @return list of directories containing native executables
       *
       * @see #setCommandPath(Path)
       */
      public Path getCommandPath() {
          return commandPath;
      }
  
      /**
       * Sets attribute containing path of directories which contain native
       * executable files to include in the created jmod.
       *
       * @param path list of directories containing native executables
       *
       * @see #createCommandPath()
       */
      public void setCommandPath(final Path path) {
          if (commandPath == null) {
              this.commandPath = path;
          } else {
              commandPath.append(path);
          }
      }
  
      /**
       * Sets {@linkplain #setCommandPath(Path) command path}
       * from a path reference.
       *
       * @param ref reference to path which will act as command path
       */
      public void setCommandPathRef(final Reference ref) {
          createCommandPath().setRefid(ref);
      }
  
      /**
       * Creates a child element which can contain a list of directories
       * containing user configuration files to include in the created jmod.
       *
       * @return new, unconfigured child element
       *
       * @see #setConfigPath(Path)
       */
      public Path createConfigPath() {
          if (configPath == null) {
              configPath = new Path(getProject());
          }
          return configPath.createPath();
      }
  
      /**
       * Attribute containing list of directories which contain
       * user configuration files.
       *
       * @return list of directories containing user configuration files
       *
       * @see #setConfigPath(Path)
       */
      public Path getConfigPath() {
          return configPath;
      }
  
      /**
       * Sets attribute containing list of directories which contain
       * user configuration files.
       *
       * @param path list of directories containing user configuration files
       *
       * @see #createConfigPath()
       */
      public void setConfigPath(final Path path) {
          if (configPath == null) {
              this.configPath = path;
          } else {
              configPath.append(path);
          }
      }
  
      /**
       * Sets {@linkplain #setConfigPath(Path) configuration file path}
       * from a path reference.
       *
       * @param ref reference to path which will act as configuration file path
       */
      public void setConfigPathRef(final Reference ref) {
          createConfigPath().setRefid(ref);
      }
  
      /**
       * Creates a child element which can contain a list of directories
       * containing compile-time header files for third party use, to include
       * in the created jmod.
       *
       * @return new, unconfigured child element
       *
       * @see #setHeaderPath(Path)
       */
      public Path createHeaderPath() {
          if (headerPath == null) {
              headerPath = new Path(getProject());
          }
          return headerPath.createPath();
      }
  
      /**
       * Attribute containing a path of directories which hold compile-time
       * header files for third party use, all of which will be included in the
       * created jmod.
       *
       * @return path of directories containing header files
       */
      public Path getHeaderPath() {
          return headerPath;
      }
  
      /**
       * Sets attribute containing a path of directories which hold compile-time
       * header files for third party use, all of which will be included in the
       * created jmod.
       *
       * @param path path of directories containing header files
       *
       * @see #createHeaderPath()
       */
      public void setHeaderPath(final Path path) {
          if (headerPath == null) {
              this.headerPath = path;
          } else {
              headerPath.append(path);
          }
      }
  
      /**
       * Sets {@linkplain #setHeaderPath(Path) header path}
       * from a path reference.
       *
       * @param ref reference to path which will act as header path
       */
      public void setHeaderPathRef(final Reference ref) {
          createHeaderPath().setRefid(ref);
      }
  
      /**
       * Creates a child element which can contain a list of directories
       * containing license files to include in the created jmod.
       *
       * @return new, unconfigured child element
       *
       * @see #setLegalPath(Path)
       */
      public Path createLegalPath() {
          if (legalPath == null) {
              legalPath = new Path(getProject());
          }
          return legalPath.createPath();
      }
  
      /**
       * Attribute containing list of directories which hold license files
       * to include in the created jmod.
       *
       * @return path containing directories which hold license files
       */
      public Path getLegalPath() {
          return legalPath;
      }
  
      /**
       * Sets attribute containing list of directories which hold license files
       * to include in the created jmod.
       *
       * @param path path containing directories which hold license files
       *
       * @see #createLegalPath()
       */
      public void setLegalPath(final Path path) {
          if (legalPath == null) {
              this.legalPath = path;
          } else {
              legalPath.append(path);
          }
      }
  
      /**
       * Sets {@linkplain #setLegalPath(Path) legal licenses path}
       * from a path reference.
       *
       * @param ref reference to path which will act as legal path
       */
      public void setLegalPathRef(final Reference ref) {
          createLegalPath().setRefid(ref);
      }
  
      /**
       * Creates a child element which can contain a list of directories
       * containing native libraries to include in the created jmod.
       *
       * @return new, unconfigured child element
       *
       * @see #setNativeLibPath(Path)
       */
      public Path createNativeLibPath() {
          if (nativeLibPath == null) {
              nativeLibPath = new Path(getProject());
          }
          return nativeLibPath.createPath();
      }
  
      /**
       * Attribute containing list of directories which hold native libraries
       * to include in the created jmod.
       *
       * @return path of directories containing native libraries
       */
      public Path getNativeLibPath() {
          return nativeLibPath;
      }
  
      /**
       * Sets attribute containing list of directories which hold native libraries
       * to include in the created jmod.
       *
       * @param path path of directories containing native libraries
       *
       * @see #createNativeLibPath()
       */
      public void setNativeLibPath(final Path path) {
          if (nativeLibPath == null) {
              this.nativeLibPath = path;
          } else {
              nativeLibPath.append(path);
          }
      }
  
      /**
       * Sets {@linkplain #setNativeLibPath(Path) native library path}
       * from a path reference.
       *
       * @param ref reference to path which will act as native library path
       */
      public void setNativeLibPathRef(final Reference ref) {
          createNativeLibPath().setRefid(ref);
      }
  
      /**
       * Creates a child element which can contain a list of directories
       * containing man pages (program manuals, typically in troff format)
       * to include in the created jmod.
       *
       * @return new, unconfigured child element
       *
       * @see #setManPath(Path)
       */
      public Path createManPath() {
          if (manPath == null) {
              manPath = new Path(getProject());
          }
          return manPath.createPath();
      }
  
      /**
       * Attribute containing list of directories containing man pages
       * to include in created jmod.  Man pages are textual program manuals,
       * typically in troff format.
       *
       * @return path containing directories which hold man pages to include
       *         in jmod
       */
      public Path getManPath() {
          return manPath;
      }
  
      /**
       * Sets attribute containing list of directories containing man pages
       * to include in created jmod.  Man pages are textual program manuals,
       * typically in troff format.
       *
       * @param path path containing directories which hold man pages to include
       *             in jmod
       *
       * @see #createManPath()
       */
      public void setManPath(final Path path) {
          if (manPath == null) {
              this.manPath = path;
          } else {
              manPath.append(path);
          }
      }
  
      /**
       * Sets {@linkplain #setManPath(Path) man pages path}
       * from a path reference.
       *
       * @param ref reference to path which will act as module path
       */
      public void setManPathRef(final Reference ref) {
          createManPath().setRefid(ref);
      }
  
      /**
       * Creates an uninitialized child element representing the version of
       * the module represented by the created jmod.
       *
       * @return new, unconfigured child element
       *
       * @see #setVersion(String)
       */
      public ModuleVersion createVersion() {
          if (moduleVersion != null) {
              throw new BuildException(
                  "No more than one <moduleVersion> element is allowed.",
                  getLocation());
          }
          moduleVersion = new ModuleVersion();
          return moduleVersion;
      }
  
      /**
       * Attribute which specifies
       * a <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/module/ModuleDescriptor.Version.html">module version</a>
       * for created jmod.
       *
       * @return module version for created jmod
       */
      public String getVersion() {
          return version;
      }
  
      /**
       * Sets the <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/module/ModuleDescriptor.Version.html">module version</a>
       * for the created jmod.
       *
       * @param version module version of created jmod
       *
       * @see #createVersion()
       */
      public void setVersion(final String version) {
          this.version = version;
      }
  
      /**
       * Attribute containing the class that acts as the executable entry point
       * of the created jmod.
       *
       * @return fully-qualified name of jmod's main class
       */
      public String getMainClass() {
          return mainClass;
      }
  
      /**
       * Sets attribute containing the class that acts as the
       * executable entry point of the created jmod.
       *
       * @param className fully-qualified name of jmod's main class
       */
      public void setMainClass(final String className) {
          this.mainClass = className;
      }
  
      /**
       * Attribute containing the platform for which the jmod
       * will be built.  Platform values are defined in the
       * {@code java.base.jmod} of JDKs, and usually take the form
       * <var>OS</var>{@code -}<var>architecture</var>.  If unset,
       * current platform is used.
       *
       * @return OS and architecture for which jmod will be built, or {@code null}
       */
      public String getPlatform() {
          return platform;
      }
  
      /**
       * Sets attribute containing the platform for which the jmod
       * will be built.  Platform values are defined in the
       * {@code java.base.jmod} of JDKs, and usually take the form
       * <var>OS</var>{@code -}<var>architecture</var>.  If unset,
       * current platform is used.
       * <p>
       * A JDK's platform can be viewed with a command like:
       * <code>jmod describe $JDK_HOME/jmods/java.base.jmod | grep -i platform</code>.
  o    *
       * @param platform platform for which jmod will be created, or {@code null}
       */
      public void setPlatform(final String platform) {
          this.platform = platform;
      }
  
      /**
       * Attribute containing a regular expression which specifies which
       * of the modules that depend on the jmod being created should have
       * hashes generated and added to the jmod.
       *
       * @return regex specifying which dependent modules should have
       *         their generated hashes included
       */
      public String getHashModulesPattern() {
          return hashModulesPattern;
      }
  
      /**
       * Sets attribute containing a regular expression which specifies which
       * of the modules that depend on the jmod being created should have
       * hashes generated and added to the jmod.
       *
       * @param pattern regex specifying which dependent modules should have
       *         their generated hashes included
       */
      public void setHashModulesPattern(final String pattern) {
          this.hashModulesPattern = pattern;
      }
  
      /**
       * Attribute indicating whether the created jmod should be visible
       * in a module path, even when not specified explicitly.  True by default.
       *
       * @return whether jmod should be visible in module paths
       */
      public boolean getResolveByDefault() {
          return resolveByDefault;
      }
  
      /**
       * Sets attribute indicating whether the created jmod should be visible
       * in a module path, even when not specified explicitly.  True by default.
       *
       * @param resolve whether jmod should be visible in module paths
       */
      public void setResolveByDefault(final boolean resolve) {
          this.resolveByDefault = resolve;
      }
  
      /**
       * Creates a child element which can specify the circumstances
       * under which jmod creation emits warnings.
       *
       * @return new, unconfigured child element
       *
       * @see #setModuleWarnings(String)
       */
      public ResolutionWarningSpec createModuleWarning() {
          ResolutionWarningSpec warningSpec = new ResolutionWarningSpec();
          moduleWarnings.add(warningSpec);
          return warningSpec;
      }
  
      /**
       * Sets attribute containing a comma-separated list of reasons for
       * jmod creation to emit warnings.  Valid values in list are:
       * {@code deprecated}, {@code leaving}, {@code incubating}.
       *
       * @param warningList list containing one or more of the above values,
       *                    separated by commas
       *
       * @see #createModuleWarning()
       * @see Jmod.ResolutionWarningReason
       */
      public void setModuleWarnings(final String warningList) {
          for (String warning : warningList.split(",")) {
              moduleWarnings.add(new ResolutionWarningSpec(warning));
          }
      }
  
      /**
       * Permissible reasons for jmod creation to emit warnings.
       */
      public static class ResolutionWarningReason
      extends EnumeratedAttribute {
          /**
           * String value indicating warnings are emitted for modules
           * marked as deprecated (but not deprecated for removal).
           */
          public static final String DEPRECATED = "deprecated";
  
          /**
           * String value indicating warnings are emitted for modules
           * marked as deprecated for removal.
           */
          public static final String LEAVING = "leaving";
  
          /**
           * String value indicating warnings are emitted for modules
           * designated as "incubating" in the JDK.
           */
          public static final String INCUBATING = "incubating";
  
          /** Maps Ant task values to jmod option values. */
          private static final Map<String, String> VALUES_TO_OPTIONS;
  
          static {
              Map<String, String> map = new LinkedHashMap<>();
              map.put(DEPRECATED, "deprecated");
              map.put(LEAVING,    "deprecated-for-removal");
              map.put(INCUBATING, "incubating");
  
              VALUES_TO_OPTIONS = Collections.unmodifiableMap(map);
          }
  
          @Override
          public String[] getValues() {
              return VALUES_TO_OPTIONS.keySet().toArray(new String[0]);
          }
  
          /**
           * Converts this object's current value to a jmod tool
           * option value.
           *
           * @return jmod option value
           */
          String toCommandLineOption() {
              return VALUES_TO_OPTIONS.get(getValue());
          }
  
          /**
           * Converts a string to a {@code ResolutionWarningReason} instance.
           *
           * @param s string to convert
           *
           * @return {@code ResolutionWarningReason} instance corresponding to
           *         string argument
           *
           * @throws BuildException if argument is not a valid
           *                        {@code ResolutionWarningReason} value
           */
          public static ResolutionWarningReason valueOf(String s) {
              return (ResolutionWarningReason)
                  getInstance(ResolutionWarningReason.class, s);
          }
      }
  
      /**
       * Child element which enables jmod tool warnings.  'reason' attribute
       * is required.
       */
      public class ResolutionWarningSpec {
          /** Condition which should trigger jmod warning output. */
          private ResolutionWarningReason reason;
  
          /**
           * Creates an uninitialized element.
           */
          public ResolutionWarningSpec() {
              // Deliberately empty.
          }
  
          /**
           * Creates an element with the given reason attribute.
           *
           * @param reason non{@code null} {@link Jmod.ResolutionWarningReason}
           *               value
           *
           * @throws BuildException if argument is not a valid
           *                        {@code ResolutionWarningReason}
           */
          public ResolutionWarningSpec(String reason) {
              setReason(ResolutionWarningReason.valueOf(reason));
          }
  
          /**
           * Required attribute containing reason for emitting jmod warnings.
           *
           * @return condition which triggers jmod warnings
           */
          public ResolutionWarningReason getReason() {
              return reason;
          }
  
          /**
           * Sets attribute containing reason for emitting jmod warnings.
           *
           * @param reason condition which triggers jmod warnings
           */
          public void setReason(ResolutionWarningReason reason) {
              this.reason = reason;
          }
  
          /**
           * Verifies this object's state.
           *
           * @throws BuildException if this object's reason is {@code null}
           */
          public void validate() {
              if (reason == null) {
                  throw new BuildException("reason attribute is required",
                      getLocation());
              }
          }
      }
  
      /**
       * Checks whether a resource is a directory.  Used for checking validity
       * of jmod path arguments which have to be directories.
       *
       * @param resource resource to check
       *
       * @return true if resource exists and is not a directory,
       *         false if it is a directory or does not exist
       */
      private static boolean isRegularFile(Resource resource) {
          return resource.isExists() && !resource.isDirectory();
      }
  
      /**
       * Checks that all paths which are required to be directories only,
       * refer only to directories.
       *
       * @throws BuildException if any path has an existing file
       *                        which is a non-directory
       */
      private void checkDirPaths() {
          if (modulePath != null
              && modulePath.stream().anyMatch(Jmod::isRegularFile)) {
  
              throw new BuildException(
                  "ModulePath must contain only directories.", getLocation());
          }
          if (commandPath != null
              && commandPath.stream().anyMatch(Jmod::isRegularFile)) {
  
              throw new BuildException(
                  "CommandPath must contain only directories.", getLocation());
          }
          if (configPath != null
              && configPath.stream().anyMatch(Jmod::isRegularFile)) {
  
              throw new BuildException(
                  "ConfigPath must contain only directories.", getLocation());
          }
          if (headerPath != null
              && headerPath.stream().anyMatch(Jmod::isRegularFile)) {
  
              throw new BuildException(
                  "HeaderPath must contain only directories.", getLocation());
          }
          if (legalPath != null
              && legalPath.stream().anyMatch(Jmod::isRegularFile)) {
  
              throw new BuildException(
                  "LegalPath must contain only directories.", getLocation());
          }
          if (nativeLibPath != null
              && nativeLibPath.stream().anyMatch(Jmod::isRegularFile)) {
  
              throw new BuildException(
                  "NativeLibPath must contain only directories.", getLocation());
          }
          if (manPath != null
              && manPath.stream().anyMatch(Jmod::isRegularFile)) {
  
              throw new BuildException(
                  "ManPath must contain only directories.", getLocation());
          }
      }
  
      /**
       * Creates a jmod file according to this task's properties
       * and child elements.
       *
       * @throws BuildException if destFile is not set
       * @throws BuildException if classpath is not set or is empty
       * @throws BuildException if any path other than classpath refers to an
       *                        existing file which is not a directory
       * @throws BuildException if both {@code version} attribute and
       *                        {@code <version>} child element are present
       * @throws BuildException if {@code hashModulesPattern} is set, but
       *                        module path is not defined
       */
      @Override
      public void execute()
      throws BuildException {
  
          if (jmodFile == null) {
              throw new BuildException("Destination file is required.",
                  getLocation());
          }
  
          if (classpath == null) {
              throw new BuildException("Classpath is required.",
                  getLocation());
          }
  
          if (classpath.stream().noneMatch(Resource::isExists)) {
              throw new BuildException(
                  "Classpath must contain at least one entry which exists.",
                  getLocation());
          }
  
          if (version != null && moduleVersion != null) {
              throw new BuildException(
                  "version attribute and nested <version> element "
                  + "cannot both be present.",
                  getLocation());
          }
  
          if (hashModulesPattern != null && !hashModulesPattern.isEmpty()
              && modulePath == null) {
  
              throw new BuildException(
                  "hashModulesPattern requires a module path, since "
                  + "it will generate hashes of the other modules which depend "
                  + "on the module being created.",
                  getLocation());
          }
  
          checkDirPaths();
  
          Path[] dependentPaths = {
              classpath,
              modulePath,
              commandPath,
              configPath,
              headerPath,
              legalPath,
              nativeLibPath,
              manPath,
          };
          Union allResources = new Union(getProject());
          for (Path path : dependentPaths) {
              if (path != null) {
                  for (String entry : path.list()) {
                      File entryFile = new File(entry);
                      if (entryFile.isDirectory()) {
                          log("Will compare timestamp of all files in "
                              + "\"" + entryFile + "\" with timestamp of "
                              + jmodFile, Project.MSG_VERBOSE);
                          FileSet fileSet = new FileSet();
                          fileSet.setDir(entryFile);
                          allResources.add(fileSet);
                      } else {
                          log("Will compare timestamp of \"" + entryFile + "\" "
                              + "with timestamp of " + jmodFile,
                              Project.MSG_VERBOSE);
                          allResources.add(new FileResource(entryFile));
                      }
                  }
              }
          }
  
          ResourceCollection outOfDate =
              ResourceUtils.selectOutOfDateSources(this, allResources,
                  new MergingMapper(jmodFile.toString()),
                  getProject(),
                  FileUtils.getFileUtils().getFileTimestampGranularity());
  
          if (outOfDate.isEmpty()) {
              log("Skipping jmod creation, since \"" + jmodFile + "\" "
                  + "is already newer than all files in paths.",
                  Project.MSG_VERBOSE);
              return;
          }
  
          Collection<String> args = buildJmodArgs();
  
          try {
              log("Deleting " + jmodFile + " if it exists.", Project.MSG_VERBOSE);
              Files.deleteIfExists(jmodFile.toPath());
          } catch (IOException e) {
              throw new BuildException(
                  "Could not remove old file \"" + jmodFile + "\": " + e, e,
                  getLocation());
          }
  
          ToolProvider jmod = ToolProvider.findFirst("jmod").orElseThrow(
              () -> new BuildException("jmod tool not found in JDK.",
                  getLocation()));
  
          log("Executing: jmod " + String.join(" ", args), Project.MSG_VERBOSE);
  
          ByteArrayOutputStream stdout = new ByteArrayOutputStream();
          ByteArrayOutputStream stderr = new ByteArrayOutputStream();
  
          int exitCode;
          try (PrintStream out = new PrintStream(stdout);
               PrintStream err = new PrintStream(stderr)) {
  
              exitCode = jmod.run(out, err, args.toArray(new String[0]));
          }
  
          if (exitCode != 0) {
              StringBuilder message = new StringBuilder();
              message.append("jmod failed (exit code ").append(exitCode).append(")");
              if (stdout.size() > 0) {
                  message.append(", output is: ").append(stdout);
              }
              if (stderr.size() > 0) {
                  message.append(", error output is: ").append(stderr);
              }
  
              throw new BuildException(message.toString(), getLocation());
          }
  
          log("Created " + jmodFile.getAbsolutePath(), Project.MSG_INFO);
      }
  
      /**
       * Creates list of arguments to <code>jmod</code> tool, based on this
       * instance's current state.
       *
       * @return new list of <code>jmod</code> arguments
       */
      private Collection<String> buildJmodArgs() {
          Collection<String> args = new ArrayList<>();
  
          args.add("create");
  
          args.add("--class-path");
          args.add(classpath.toString());
  
          // Paths
  
          if (modulePath != null && !modulePath.isEmpty()) {
              args.add("--module-path");
              args.add(modulePath.toString());
          }
          if (commandPath != null && !commandPath.isEmpty()) {
              args.add("--cmds");
              args.add(commandPath.toString());
          }
          if (configPath != null && !configPath.isEmpty()) {
              args.add("--config");
              args.add(configPath.toString());
          }
          if (headerPath != null && !headerPath.isEmpty()) {
              args.add("--header-files");
              args.add(headerPath.toString());
          }
          if (legalPath != null && !legalPath.isEmpty()) {
              args.add("--legal-notices");
              args.add(legalPath.toString());
          }
          if (nativeLibPath != null && !nativeLibPath.isEmpty()) {
              args.add("--libs");
              args.add(nativeLibPath.toString());
          }
          if (manPath != null && !manPath.isEmpty()) {
              args.add("--man-pages");
              args.add(manPath.toString());
          }
  
          // Strings
  
          String versionStr =
              (moduleVersion != null ? moduleVersion.toModuleVersionString() : version);
          if (versionStr != null && !versionStr.isEmpty()) {
              args.add("--module-version");
              args.add(versionStr);
          }
  
          if (mainClass != null && !mainClass.isEmpty()) {
              args.add("--main-class");
              args.add(mainClass);
          }
          if (platform != null && !platform.isEmpty()) {
              args.add("--target-platform");
              args.add(platform);
          }
          if (hashModulesPattern != null && !hashModulesPattern.isEmpty()) {
              args.add("--hash-modules");
              args.add(hashModulesPattern);
          }
  
          // booleans
  
          if (!resolveByDefault) {
              args.add("--do-not-resolve-by-default");
          }
          for (ResolutionWarningSpec moduleWarning : moduleWarnings) {
              moduleWarning.validate();
              args.add("--warn-if-resolved");
              args.add(moduleWarning.getReason().toCommandLineOption());
          }
  
          // Destination file
  
          args.add(jmodFile.toString());
  
          return args;
      }
  }
  