public class BuildException extends Exception {
    private static final long serialVersionUID = 1520625837859688798L;

    public BuildException(String message) {
        super(message);
    }

    public BuildException(String message, Throwable cause) {
        super(message, cause);
    }

    public BuildException(Throwable cause) {
        super(cause);
    }

} public class BuildRequest {
    /**
     *
     * A builder for {@link BuildRequest}s.
     *
     */
    public static class BuildRequestBuilder {

        private boolean addDefaultBuildArguments = true;
        private boolean addDefaultBuildEnvironment = true;
        private List<String> buildArguments = new ArrayList<>();
        private Map<String, String> buildEnvironment = new LinkedHashMap<>();
        private Set<Ga> buildIncludes = new LinkedHashSet<>();
        private Path dependentProjectRootDirectory;
        private Charset encoding;
        private boolean excludeNonRequired = false;
        private Set<String> forwardPropertyNames = new LinkedHashSet<>();
        private Map<String, String> forwardPropertyValues = new LinkedHashMap<>();
        private GavSet gavSet = GavSet.includeAll();
        private CharStreamSource gradleModelTransformer;
        private Supplier<LineConsumer> output;
        private Path projectRootDirectory;
        private String scmRepositoryId;
        private List<String> scmUrls = new ArrayList<>();
        private boolean skipTests = true;
        private SrcVersion srcVersion;
        private long timeoutMs = DEFAULT_TIMEOUT_MS;
        private boolean useVersionsMavenPlugin = false;
        private Verbosity verbosity = Verbosity.info;
        private String version;
        private String versionsMavenPluginVersion;

        /**
         * @param addDefaultBuildArguments see {@link BuildRequest#isAddDefaultBuildArguments()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder addDefaultBuildArguments(boolean addDefaultBuildArguments) {
            this.addDefaultBuildArguments = addDefaultBuildArguments;
            return this;
        }

        /**
         * @param addDefaultBuildEnvironment see {@link BuildRequest#isAddDefaultBuildEnvironment()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder addDefaultBuildEnvironment(boolean addDefaultBuildEnvironment) {
            this.addDefaultBuildEnvironment = addDefaultBuildEnvironment;
            return this;
        }

        /**
         * @return a new {@link BuildRequest} based on the values stored in fields of this {@link BuildRequestBuilder}
         */
        public BuildRequest build() {
            final String useVersion = version == null ? srcVersion.toString() : version;

            final Set<String> useFwdPropNames = Collections.unmodifiableSet(forwardPropertyNames);
            this.forwardPropertyNames = null;

            final Map<String, String> useFwdPropValues = Collections.unmodifiableMap(forwardPropertyValues);
            this.forwardPropertyValues = null;

            final List<String> useScmUrls = Collections.unmodifiableList(scmUrls);
            this.scmUrls = null;

            final List<String> useBuildArgs = Collections.unmodifiableList(buildArguments);
            this.buildArguments = null;

            final Map<String, String> useBuildEnv = Collections.unmodifiableMap(buildEnvironment);
            this.buildEnvironment = null;

            final Set<Ga> useBuildIncludes = Collections.unmodifiableSet(buildIncludes);
            this.buildIncludes = null;

            return new BuildRequest(dependentProjectRootDirectory, projectRootDirectory, srcVersion, useVersion, gavSet,
                    scmRepositoryId, encoding, useScmUrls, useBuildArgs, skipTests, addDefaultBuildArguments,
                    useFwdPropNames, useFwdPropValues, useBuildEnv, addDefaultBuildEnvironment, verbosity, output,
                    timeoutMs, versionsMavenPluginVersion, useVersionsMavenPlugin, useBuildIncludes, excludeNonRequired,
                    gradleModelTransformer);
        }

        /**
         * @param argument the single build argument to add to {@link #buildArguments}; see
         *        {@link BuildRequest#getBuildArguments()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder buildArgument(String argument) {
            this.buildArguments.add(argument);
            return this;
        }

        /**
         * @param arguments the arguments to add to {@link #buildArguments}; see
         *        {@link BuildRequest#getBuildArguments()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder buildArguments(List<String> buildArguments) {
            this.buildArguments.addAll(buildArguments);
            return this;
        }

        /**
         * @param arguments the arguments to add to {@link #buildArguments}; see
         *        {@link BuildRequest#getBuildArguments()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder buildArguments(String... arguments) {
            for (String argument : arguments) {
                this.buildArguments.add(argument);
            }
            return this;
        }

        /**
         * @param buildEnvironment see {@link BuildRequest#getBuildEnvironment()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder buildEnvironment(Map<String, String> buildEnvironment) {
            this.buildEnvironment.putAll(buildEnvironment);
            return this;
        }

        /**
         * Add an environment variable of the given {@code name} and {@code value} to {@link #buildEnvironment}.
         *
         * @see BuildRequest#getBuildEnvironment()
         * @param name the name of the environment variable to add to {@link #buildEnvironment}
         * @param value the value of the environment variable to add to {@link #buildEnvironment}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder buildEnvironmentVariable(String name, String value) {
            this.buildEnvironment.put(name, value);
            return this;
        }

        /**
         * @see BuildRequest#getBuildIncludes()
         * @param ga a {@link Ga} to include
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder buildInclude(Ga ga) {
            buildIncludes.add(ga);
            return this;
        }

        /**
         * @see BuildRequest#getBuildIncludes()
         * @param gas a {@link Collection} of {@link Ga} identifiers
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder buildIncludes(Collection<Ga> gas) {
            buildIncludes.addAll(gas);
            return this;
        }

        /**
         * @param dependentProjectRootDirectory see {@link BuildRequest#getDependentProjectRootDirectory()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder dependentProjectRootDirectory(Path dependentProjectRootDirectory) {
            this.dependentProjectRootDirectory = dependentProjectRootDirectory;
            return this;
        }

        /**
         * @param encoding see {@link BuildRequest#getEncoding()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder encoding(Charset encoding) {
            this.encoding = encoding;
            return this;
        }

        /**
         * @param excludeNonRequired see {@link BuildRequest#isExcludeNonRequired()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder excludeNonRequired(boolean excludeNonRequired) {
            this.excludeNonRequired = excludeNonRequired;
            return this;
        }

        /**
         * @see BuildRequest#getForwardPropertyNames()
         * @param value the property name or pattern to forward
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder forwardPropertyName(String value) {
            forwardPropertyNames.add(value);
            return this;
        }

        /**
         * @see BuildRequest#getForwardPropertyNames()
         * @param values the property names or patterns to forward
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder forwardPropertyNames(Collection<String> values) {
            forwardPropertyNames.addAll(values);
            return this;
        }

        /**
         * @see BuildRequest#getForwardPropertyValues()
         * @param key the property key to forward
         * @param value the property value to forward
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder forwardPropertyValue(String key, String value) {
            forwardPropertyValues.put(key, value);
            return this;
        }

        /**
         * @see BuildRequest#getForwardPropertyValues()
         * @param values the properties to forward
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder forwardPropertyValues(Map<String, String> values) {
            forwardPropertyValues.putAll(values);
            return this;
        }

        /**
         * @see BuildRequest#getGavSet()
         * @param gavSet the {@link GavSet} to set
         * @return this builder
         */
        public BuildRequestBuilder gavSet(GavSet gavSet) {
            this.gavSet = gavSet;
            return this;
        }

        /**
         * @param gradleModelTransformer see {@link BuildRequest#getGradleModelTransformer()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder gradleModelTransformer(CharStreamSource gradleModelTransformer) {
            this.gradleModelTransformer = gradleModelTransformer;
            return this;
        }

        /**
         * @param output see {@link BuildRequest#getOuput()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder output(Supplier<LineConsumer> output) {
            this.output = output;
            return this;
        }

        /**
         * @param projectRootDirectory see {@link BuildRequest#getProjectRootDirectory()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder projectRootDirectory(Path projectRootDirectory) {
            this.projectRootDirectory = projectRootDirectory;
            return this;
        }

        /**
         * @see BuildRequest#getScmRepositoryId()
         * @param scmRepositoryId the ID of the SCM repository to set
         * @return this builder
         */
        public BuildRequestBuilder scmRepositoryId(String scmRepositoryId) {
            this.scmRepositoryId = scmRepositoryId;
            return this;
        }

        /**
         * @param scmUrl a SCM url to add to {@link #scmUrls}. See {@link BuildRequest#getScmUrls()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder scmUrl(String scmUrl) {
            this.scmUrls.add(scmUrl);
            return this;
        }

        /**
         * @param scmUrls SCM urls to add to {@link #scmUrls}. See {@link BuildRequest#getScmUrls()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder scmUrls(Collection<String> scmUrls) {
            this.scmUrls.addAll(scmUrls);
            return this;
        }

        /**
         * @param skipTests see {@link BuildRequest#isSkipTests()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder skipTests(boolean skipTests) {
            this.skipTests = skipTests;
            return this;
        }

        /**
         * @param srcVersion see {@link BuildRequest#getSrcVersion()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder srcVersion(SrcVersion srcVersion) {
            this.srcVersion = srcVersion;
            return this;
        }

        /**
         * @param timeoutMs see {@link BuildRequest#getTimeoutMs()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder timeoutMs(long timeoutMs) {
            this.timeoutMs = timeoutMs;
            return this;
        }

        public BuildRequestBuilder useVersionsMavenPlugin(boolean useVersionsMavenPlugin) {
            this.useVersionsMavenPlugin = useVersionsMavenPlugin;
            return this;
        }

        /**
         * @param verbosity see {@link BuildRequest#getVerbosity()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder verbosity(Verbosity verbosity) {
            this.verbosity = verbosity;
            return this;
        }

        /**
         * If {@link #version} is not set using this method, {@link #srcVersion}.toString() will be used.
         *
         * @param version see {@link BuildRequest#getVerbosity()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder version(String version) {
            this.version = version;
            return this;
        }

        /**
         * @param versionsMavenPluginVersion see {@link BuildRequest#getVersionsMavenPluginVersion()}
         * @return this {@link BuildRequestBuilder}
         */
        public BuildRequestBuilder versionsMavenPluginVersion(String versionsMavenPluginVersion) {
            this.versionsMavenPluginVersion = versionsMavenPluginVersion;
            return this;
        }
    }

    /**
     * The verbosity level the appropriate {@link Builder} should use when executing a {@link BuildRequest}. The
     * interpretation of the individual levels is up to the given {@link Builder} implementation. Some {@link Builder}s
     * may map the levels listed here to a distinct set of levels they support internally.
     */
    public enum Verbosity {
        debug, error, info, trace, warn;

        public static Verbosity fastValueOf(String level) {
            SrcdepsCoreUtils.assertArgNotNull(level, "Verbosity name");
            switch (level.toLowerCase(Locale.ROOT)) {
            case "trace":
                return trace;
            case "debug":
                return debug;
            case "info":
                return info;
            case "warn":
                return warn;
            case "error":
                return error;
            default:
                throw new IllegalStateException("No such " + Verbosity.class.getName() + " with name [" + level + "]");
            }
        }

    }

    /** {@link Long#MAX_VALUE} */
    private static final long DEFAULT_TIMEOUT_MS = Long.MAX_VALUE;

    private static final Logger log = LoggerFactory.getLogger(BuildRequest.class);

    /**
     * @return a new {@link BuildRequestBuilder}
     */
    public static BuildRequestBuilder builder() {
        return new BuildRequestBuilder();
    }

    /**
     * Computes a hash out of the given {@link BuildRequest} fields. The fields are the ones whose change should trigger
     * a rebuild of the underlying source repository.
     *
     * @param addDefaultBuildArguments
     * @param addDefaultBuildEnvironment
     * @param buildArguments
     * @param buildEnvironment
     * @param forwardProperties
     * @param encoding
     * @param gavSet
     * @param scmUrls
     * @param skipTests
     * @param srcVersion
     * @param version
     * @param useVersionsMavenPlugin
     * @param timeoutMs
     * @param verbosity
     * @return a sha1 hash in hex form
     */
    public static String computeHash(boolean addDefaultBuildArguments, boolean addDefaultBuildEnvironment,
            List<String> buildArguments, Map<String, String> buildEnvironment, Set<String> forwardProperties,
            Charset encoding, GavSet gavSet, List<String> scmUrls, boolean skipTests, SrcVersion srcVersion,
            String version, boolean useVersionsMavenPlugin, Set<Ga> buildIncludes, boolean excludeNonRequired,
            long timeoutMs, Verbosity verbosity) {

        try (DigestOutputStream digester = new DigestOutputStream(MessageDigest.getInstance("SHA-1"))) {
            digester.write(addDefaultBuildArguments ? 1 : 0);
            digester.write(addDefaultBuildEnvironment ? 1 : 0);
            digester.write(skipTests ? 1 : 0);
            ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
            buffer.putLong(timeoutMs);
            digester.write(buffer.array());
            digester.flush();
            final Charset utf8 = StandardCharsets.UTF_8;
            try (Writer w = new OutputStreamWriter(digester, utf8)) {
                for (String e : buildArguments) {
                    w.write(e);
                }
                for (Map.Entry<String, String> e : buildEnvironment.entrySet()) {
                    w.write(e.getKey());
                    w.write(e.getValue());
                }
                for (String e : forwardProperties) {
                    w.write(e);
                }

                w.write(encoding.name());

                gavSet.appendIncludes(w);
                gavSet.appendExcludes(w);
                for (String e : scmUrls) {
                    w.write(e);
                }

                w.write(srcVersion.toString());
                w.write(version);
                w.write(useVersionsMavenPlugin ? 1 : 0);
                for (Ga e : buildIncludes) {
                    w.write(e.toString());
                }
                digester.write(excludeNonRequired ? 1 : 0);
                w.write(verbosity.name());
            }
            final byte[] sha1Bytes = digester.digest();
            return SrcdepsCoreUtils.bytesToHexString(sha1Bytes);
        } catch (IOException | NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * @return the default timeout in milliseconds for source dependency builds. The value is {@link Long#MAX_VALUE}.
     */
    public static long getDefaultTimeoutMs() {
        return DEFAULT_TIMEOUT_MS;
    }

    private final boolean addDefaultBuildArguments;
    private final boolean addDefaultBuildEnvironment;
    private final List<String> buildArguments;
    private final Map<String, String> buildEnvironment;
    private final Set<Ga> buildIncludes;
    private final Path dependentProjectRootDirectory;
    private final Charset encoding;
    private final boolean excludeNonRequired;
    private final Set<String> forwardPropertyNames;
    private final Map<String, String> forwardPropertyValues;
    private final GavSet gavSet;
    private final CharStreamSource gradleModelTransformer;
    private final String hash;
    private final Supplier<LineConsumer> output;
    private final Path projectRootDirectory;
    private final String scmRepositoryId;
    private final List<String> scmUrls;
    private final boolean skipTests;
    private final SrcVersion srcVersion;
    private final long timeoutMs;
    private final boolean useVersionsMavenPlugin;
    private final Verbosity verbosity;
    private final String version;

    private final String versionsMavenPluginVersion;

    private BuildRequest(Path dependentProjectRootDirectory, Path projectRootDirectory, SrcVersion srcVersion,
            String version, GavSet gavSet, String scmRepositoryId, Charset encoding, List<String> scmUrls,
            List<String> buildArguments, boolean skipTests, boolean addDefaultBuildArguments,
            Set<String> forwardPropertyNames, Map<String, String> forwardPropertyValues,
            Map<String, String> buildEnvironment, boolean addDefaultBuildEnvironment, Verbosity verbosity,
            Supplier<LineConsumer> output, long timeoutMs, String versionsMavenPluginVersion,
            boolean useVersionsMavenPlugin, Set<Ga> buildIncludes, boolean excludeNonRequired,
            CharStreamSource gradleModelTransformer) {
        super();

        SrcdepsCoreUtils.assertArgNotNull(scmRepositoryId, "scmRepositoryId");
        SrcdepsCoreUtils.assertArgNotNull(encoding, "encoding");
        SrcdepsCoreUtils.assertArgNotNull(dependentProjectRootDirectory, "dependentProjectRootDirectory");
        SrcdepsCoreUtils.assertArgNotNull(projectRootDirectory, "projectRootDirectory");
        SrcdepsCoreUtils.assertArgNotNull(srcVersion, "srcVersion");
        SrcdepsCoreUtils.assertArgNotNull(version, "version");
        SrcdepsCoreUtils.assertArgNotNull(scmUrls, "scmUrls");
        SrcdepsCoreUtils.assertCollectionNotEmpty(scmUrls, "scmUrls");
        SrcdepsCoreUtils.assertArgNotNull(buildArguments, "buildArguments");
        SrcdepsCoreUtils.assertArgNotNull(forwardPropertyNames, "forwardPropertyNames");
        SrcdepsCoreUtils.assertArgNotNull(forwardPropertyValues, "forwardPropertyValues");
        SrcdepsCoreUtils.assertArgNotNull(buildEnvironment, "buildEnvironment");
        SrcdepsCoreUtils.assertArgNotNull(output, "output");
        SrcdepsCoreUtils.assertArgNotNull(versionsMavenPluginVersion, "versionsMavenPluginVersion");
        SrcdepsCoreUtils.assertArgNotNull(buildIncludes, "buildIncludes");
        SrcdepsCoreUtils.assertArgNotNull(gradleModelTransformer, "gradleModelTransformer");

        this.dependentProjectRootDirectory = dependentProjectRootDirectory;
        this.projectRootDirectory = projectRootDirectory;
        this.srcVersion = srcVersion;
        this.version = version;
        this.gavSet = gavSet;
        this.scmRepositoryId = scmRepositoryId;
        this.encoding = encoding;
        this.scmUrls = scmUrls;
        this.buildArguments = buildArguments;
        this.skipTests = skipTests;
        this.buildEnvironment = buildEnvironment;
        this.addDefaultBuildEnvironment = addDefaultBuildEnvironment;
        this.verbosity = verbosity;
        this.timeoutMs = timeoutMs;
        this.addDefaultBuildArguments = addDefaultBuildArguments;
        this.forwardPropertyNames = forwardPropertyNames;
        this.forwardPropertyValues = forwardPropertyValues;
        this.output = output;
        this.versionsMavenPluginVersion = versionsMavenPluginVersion;
        this.useVersionsMavenPlugin = useVersionsMavenPlugin;
        this.buildIncludes = buildIncludes;
        this.excludeNonRequired = excludeNonRequired;
        this.gradleModelTransformer = gradleModelTransformer;
        this.hash = computeHash(addDefaultBuildArguments, addDefaultBuildEnvironment, buildArguments, buildEnvironment,
                forwardPropertyNames, encoding, gavSet, scmUrls, skipTests, srcVersion, versionsMavenPluginVersion,
                useVersionsMavenPlugin, buildIncludes, excludeNonRequired, timeoutMs, verbosity);
        log.debug("srcdeps[{}]: Computed hash [{}] of [{}]", scmRepositoryId, hash, this);
    }

    /**
     * @return a {@link List} of arguments that should be used by {@link Builder#build(BuildRequest)}. Cannot be
     *         {@code null}. The {@link Builder} will combine this list with its default list of arguments if
     *         {@link #isAddDefaultBuildArguments()} returns {@code true}.
     */
    public List<String> getBuildArguments() {
        return buildArguments;
    }

    /**
     * @return a {@link Map} of environment variables that should be used by {@link Builder#build(BuildRequest)}. Cannot
     *         be {@code null}. Note that these are just overlay variables - if the {@link Builder} spawns a new
     *         process, the environment is copied from the present process and the variables provided by the present
     *         method are overwritten or added.
     */
    public Map<String, String> getBuildEnvironment() {
        return buildEnvironment;
    }

    /**
     * @return a list of {@link Ga} identifiers to translate to module directories and pass as {@code -pl} Maven command
     *         line option
     * @see ScmRepositoryMaven#getIncludes()
     */
    public Set<Ga> getBuildIncludes() {
        return buildIncludes;
    }

    /**
     * @return an absolute {@link Path} to the root directory of the dependent (outer) project tree.
     */
    public Path getDependentProjectRootDirectory() {
        return dependentProjectRootDirectory;
    }

    /**
     * @return the encoding to use when reading source files in the dependency's source tree
     */
    public Charset getEncoding() {
        return encoding;
    }

    /**
     * A list of property names that the top level builder A should pass as java system properties to every dependency
     * builder B using {@code -DmyProperty=myValue} style command line arguments. Further, in case a child builder B
     * spawns its own new child builder C, B must pass all these properties to C in the very same manner as A did to B.
     *
     * A property name may end with asterisk {@code *} to denote that all properties starting with the part before the
     * asterisk should be forwared. E.g. {@code my.prop.*} would forward both {@code my.prop.foo} and
     * {@code my.prop.bar}.
     *
     * @return a {@link Set} of property names to forward
     */
    public Set<String> getForwardPropertyNames() {
        return forwardPropertyNames;
    }

    /**
     * A map of property values that the top level builder A should pass as java system properties to every dependency
     * builder B using {@code -DmyProperty=myValue} style command line arguments. Further, in case a child builder B
     * spawns its own new child builder C, B must pass all these properties to C in the very same manner as A did to B.
     *
     * @return a {@link Map} of properties to forward
     */
    public Map<String, String> getForwardPropertyValues() {
        return forwardPropertyValues;
    }

    /**
     * Returns the {@link GavSet} that defines the set of artifacts that should be built by this {@link BuildRequest}.
     * {@link Builder} implementations should make their best to build just this set of artifacts and nothing else.
     *
     * @return the {@link GavSet}
     */
    public GavSet getGavSet() {
        return gavSet;
    }

    /**
     * @return a {@link CharStreamSource} from which the Gradle model transformer script will be loaded.
     */
    public CharStreamSource getGradleModelTransformer() {
        return gradleModelTransformer;
    }

    /**
     * @return a hash over the values of only those fields whose changes require a rebuild
     * @since 3.2.2
     */
    public String getHash() {
        return hash;
    }

    /**
     * @return the {@link Supplier} to to create a {@link LineConsumer} for storing the output (stdin and stderr) of the
     *         build process
     * @since 5.0.0
     */
    public Supplier<LineConsumer> getOuput() {
        return output;
    }

    /**
     * @return the root directory of the dependency project's source tree that should be built
     */
    public Path getProjectRootDirectory() {
        return projectRootDirectory;
    }

    /**
     * @return the ID of the underlying SCm repository. See {@link ScmRepository#getId()}.
     */
    public String getScmRepositoryId() {
        return scmRepositoryId;
    }

    /**
     * @return a {@link List} of URLs that should be tried one after another to checkout the version determined by
     *         {@link #getSrcVersion()}. The URLs will be tried by the {@link BuildService} in the given order until the
     *         checkout is successful. See {@link Scm} for the information about the format of the URLs.
     */
    public List<String> getScmUrls() {
        return scmUrls;
    }

    /**
     * @return the {@link SrcVersion} to checkout
     */
    public SrcVersion getSrcVersion() {
        return srcVersion;
    }

    /**
     * @return the timeout in milliseconds for the {@link Builder#setVersions(BuildRequest)} and
     *         {@link Builder#build(BuildRequest)} operations.
     */
    public long getTimeoutMs() {
        return timeoutMs;
    }

    /**
     * @return the verbosity level to use when configuring the {@link Builder}
     */
    public Verbosity getVerbosity() {
        return verbosity;
    }

    /**
     * @return the version of dependency artifacts to install to Local Maven Repository
     */
    public String getVersion() {
        return version;
    }

    /**
     * @return the version of {@code versions-maven-plugin} to use when setting versions in an inner Maven build.
     */
    public String getVersionsMavenPluginVersion() {
        return versionsMavenPluginVersion;
    }

    /**
     * @return {@code true} if the given {@link Builder}'s default arguments should be combined with the arguments
     *         returned by {@link #getBuildArguments()}; {@code false} otherwise
     */
    public boolean isAddDefaultBuildArguments() {
        return addDefaultBuildArguments;
    }

    /**
     * @return {@code true} if the given {@link Builder}'s default environment variables should be combined with the
     *         environment returned by {@link #getBuildEnvironment()}; {@code false} otherwise
     */
    public boolean isAddDefaultBuildEnvironment() {
        return addDefaultBuildEnvironment;
    }

    /**
     * If this method returns {@code true} and {@link #getBuildIncludes()} returns a non-empty list, {@code srcdeps}
     * will remove uneeded {@code <module>} elements from {@code pom.xml} files in the dependency project before
     * building it. Otherwise, the value returned by this method has no effect.
     *
     * @return {@code true} or {@code false}
     * @see ScmRepositoryMaven#isExcludeNonRequired()
     */
    public boolean isExcludeNonRequired() {
        return excludeNonRequired;
    }

    /**
     * @return {@code true} if no tests should be run when building the dependency. For dependencies built with Maven,
     *         this accounts to adding {@code -DskipTests} to the {@code mvn} arguments.
     */
    public boolean isSkipTests() {
        return skipTests;
    }

    /**
     * @return {@code true} if {@code mvn versions:set -DnewVersion...} should be used to set version in the dependency
     *         source tree. Otherwise {@link MavenSourceTree#setVersions(String, java.util.function.Predicate)} will be
     *         used. {@link MavenSourceTree#setVersions(String, java.util.function.Predicate)} should be preferred
     *         because it is faster.
     */
    public boolean isUseVersionsMavenPlugin() {
        return useVersionsMavenPlugin;
    }

    @Override
    public String toString() {
        return "BuildRequest [scmRepositoryId=" + scmRepositoryId + ", encoding=" + encoding
                + ", addDefaultBuildArguments=" + addDefaultBuildArguments + ", addDefaultBuildEnvironment="
                + addDefaultBuildEnvironment + ", buildArguments=" + buildArguments + ", buildEnvironment="
                + buildEnvironment + ", dependentProjectRootDirectory=" + dependentProjectRootDirectory
                + ", forwardPropertyNames=" + forwardPropertyNames + ", forwardPropertyValues=" + forwardPropertyValues
                + ", gavSet=" + gavSet + ", gradleModelTransformer=" + gradleModelTransformer + ", id=" + hash
                + ", output=" + output + ", projectRootDirectory=" + projectRootDirectory + ", scmUrls=" + scmUrls
                + ", skipTests=" + skipTests + ", srcVersion=" + srcVersion + ", timeoutMs=" + timeoutMs
                + ", verbosity=" + verbosity + ", version=" + version + ", versionsMavenPluginVersion="
                + versionsMavenPluginVersion + ", useVersionsMavenPlugin=" + useVersionsMavenPlugin + " buildIncludes="
                + buildIncludes + ", excludeNonRequired=" + excludeNonRequired + "]";
    }

} public enum Verbosity {
    debug, error, info, trace, warn;

    public static Verbosity fastValueOf(String level) {
        SrcdepsCoreUtils.assertArgNotNull(level, "Verbosity name");
        switch (level.toLowerCase(Locale.ROOT)) {
        case "trace":
            return trace;
        case "debug":
            return debug;
        case "info":
            return info;
        case "warn":
            return warn;
        case "error":
            return error;
        default:
            throw new IllegalStateException("No such " + Verbosity.class.getName() + " with name [" + level + "]");
        }
    }

} public class Ga implements Comparable<Ga> {

    /**
     * Returns a new {@link Ga} instance parsed out of the given {@code gavString}.
     *
     * @param gavString the string to parse, something of the form {@code groupId:artifactId:version}
     * @return a new {@link Ga} instance parsed out of the given {@code gavString}
     */
    public static Ga of(String gavString) {
        StringTokenizer st = new StringTokenizer(gavString, ":");
        if (!st.hasMoreTokens()) {
            throw new IllegalStateException(String.format("Cannot parse [%s] to a " + Ga.class.getName(), gavString));
        } else {
            final String g = st.nextToken();
            if (!st.hasMoreTokens()) {
                throw new IllegalStateException(
                        String.format("Cannot parse [%s] to a " + Ga.class.getName(), gavString));
            } else {
                final String a = st.nextToken();
                return new Ga(g, a);
            }
        }
    }

    private final String artifactId;
    private final String groupId;
    private final int hashCode;

    public Ga(String groupId, String artifactId) {
        super();
        this.groupId = groupId;
        this.artifactId = artifactId;
        this.hashCode = 31 * (31 + artifactId.hashCode()) + groupId.hashCode();
    }

    @Override
    public int compareTo(Ga o) {
        int result = this.groupId.compareTo(o.groupId);
        if (result != 0) {
            return result;
        } else {
            return this.artifactId.compareTo(o.artifactId);
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Ga other = (Ga) obj;
        return this.artifactId.equals(other.artifactId) && this.groupId.equals(other.groupId);
    }

    public String getArtifactId() {
        return artifactId;
    }

    public String getGroupId() {
        return groupId;
    }

    @Override
    public int hashCode() {
        return hashCode;
    }

    @Override
    public String toString() {
        return groupId + ":" + artifactId;
    }
} public class MavenSourceTree {
    public static class ActiveProfiles implements Predicate<Profile> {

        static final Predicate<Profile> EMPTY = new ActiveProfiles();

        /**
         * @param profileIds the active profiles (can be empty)
         * @return a new {@link Profile} filter which will hold the named {@code profileIds} for active
         */
        public static Predicate<Profile> of(String... profileIds) {
            return profileIds.length == 0 ? EMPTY : new ActiveProfiles(profileIds);
        }

        /**
         * @param args Maven command line arguments
         * @return a new {@link Predicate}
         */
        public static Predicate<Profile> ofArgs(List<String> args) {
            for (Iterator<String> it = args.iterator(); it.hasNext();) {
                final String arg = it.next();
                if ("-P".equals(arg) || "--activate-profiles".equals(arg)) {
                    return of(it.next().split(","));
                } else if (arg.startsWith("-P")) {
                    return of(arg.substring(2).split(","));
                }
            }
            return EMPTY;
        }

        final Set<String> profileIds;

        ActiveProfiles(String... profileIds) {
            super();
            Set<String> m = new LinkedHashSet<>(profileIds.length);
            for (String profileId : profileIds) {
                m.add(profileId);
            }
            this.profileIds = m;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            ActiveProfiles other = (ActiveProfiles) obj;
            if (profileIds == null) {
                if (other.profileIds != null)
                    return false;
            } else if (!profileIds.equals(other.profileIds))
                return false;
            return true;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((profileIds == null) ? 0 : profileIds.hashCode());
            return result;
        }

        @Override
        public boolean test(Profile t) {
            return t.getId() == null || profileIds.contains(t.getId());
        }

    }

    /**
     * A {@link MavenSourceTree} builder.
     */
    static class Builder {

        private final Charset encoding;

        final Map<Ga, Module.Builder> modulesByGa = new LinkedHashMap<>();

        /** By pom.xml path relative to {@link MavenSourceTree#rootDirectory} */
        final Map<String, Module.Builder> modulesByPath = new LinkedHashMap<>();

        private final Path rootDirectory;

        Builder(Path rootDirectory, Charset encoding) {
            super();
            this.rootDirectory = rootDirectory;
            this.encoding = encoding;
        }

        public MavenSourceTree build() {

            final Map<String, Module> byPath = new LinkedHashMap<>(modulesByPath.size());
            final Map<Ga, Module> byGa = new LinkedHashMap<>(modulesByPath.size());
            for (org.srcdeps.core.MavenSourceTree.Module.Builder e : modulesByPath.values()) {
                final Module module = e.build();
                byGa.put(module.getGav().resolveGa(null, null), module);
                byPath.put(module.pomPath, module);
            }
            return new MavenSourceTree(rootDirectory, encoding, Collections.unmodifiableMap(byPath),
                    Collections.unmodifiableMap(byGa));
        }

        Builder pomXml(final Path pomXml) {
            final Module.Builder module = new Module.Builder(rootDirectory, pomXml, encoding);
            modulesByPath.put(module.pomPath, module);
            modulesByGa.put(module.moduleGav.getGa(), module);
            for (Profile.Builder profile : module.profiles) {
                for (String path : profile.children) {
                    if (!modulesByPath.containsKey(path)) {
                        pomXml(rootDirectory.resolve(path));
                    }
                }
            }
            return this;
        }
    }

    public static class Dependency extends GavExpression {
        private final String scope;

        public Dependency(Expression groupId, Expression artifactId, Expression version, String scope) {
            super(groupId, artifactId, version);
            this.scope = scope;
        }

        public String getScope() {
            return scope;
        }
    }

    /**
     * An edit operation on a text element of an XML {@link Document}.
     */
    static class DomEdit {
        private final String newValue;
        private final String selector;

        DomEdit(String selector, String newValue) {
            super();
            this.selector = selector;
            this.newValue = newValue;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            DomEdit other = (DomEdit) obj;
            if (newValue == null) {
                if (other.newValue != null)
                    return false;
            } else if (!newValue.equals(other.newValue))
                return false;
            if (selector == null) {
                if (other.selector != null)
                    return false;
            } else if (!selector.equals(other.selector))
                return false;
            return true;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((newValue == null) ? 0 : newValue.hashCode());
            result = prime * result + ((selector == null) ? 0 : selector.hashCode());
            return result;
        }

        /**
         * Performs this operation.
         *
         * @param xPath {@link XPath} to use when looking up {@link Node}s
         * @param document the {@link Document} to perform this operation on
         * @param path the file system path to the given {@code document}
         */
        public void perform(XPath xPath, Node document, String path) {
            try {
                final NodeList nodes = (NodeList) xPath.evaluate(selector, document, XPathConstants.NODESET);
                if (nodes == null || nodes.getLength() == 0) {
                    throw new IllegalStateException(
                            String.format("Xpath expression [%s] did not select any nodes in [%s]", selector, path));
                }
                for (int i = 0; i < nodes.getLength(); i++) {
                    nodes.item(i).setTextContent(newValue);
                }
            } catch (XPathExpressionException | DOMException e) {
                throw new IllegalStateException(String.format("Could not evaluate [%s] on [%s]", selector, path));
            }
        }
    }

    /**
     * A set of {@link DomEdit}s.
     */
    static class DomEdits {
        final Map<String, Set<DomEdit>> domEditsByPath = new LinkedHashMap<>();

        /**
         * @param path a file system path to a {@code pom.xml} file relative to {@link MavenSourceTree#rootDirectory}
         * @param domEdit the operation to add
         */
        public void add(String path, DomEdit domEdit) {
            Set<DomEdit> edits = domEditsByPath.get(path);
            if (edits == null) {
                domEditsByPath.put(path, edits = new LinkedHashSet<MavenSourceTree.DomEdit>());
            }
            edits.add(domEdit);
        }

        /**
         * Perform the operations added via {@link #add(String, DomEdit)}.
         *
         * @param rootDirectory
         * @param encoding
         */
        public void perform(Path rootDirectory, Charset encoding) {
            try {
                final Transformer transformer = TransformerFactory.newInstance().newTransformer();
                final XPath xPath = XPathFactory.newInstance().newXPath();
                for (Entry<String, Set<DomEdit>> e : domEditsByPath.entrySet()) {
                    final Path pomXml = rootDirectory.resolve(e.getKey());
                    final Node doc = readDom(transformer, pomXml, encoding);
                    for (DomEdit edit : e.getValue()) {
                        edit.perform(xPath, doc, e.getKey());
                    }
                    writeDom(transformer, pomXml, doc, encoding);
                }
            } catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * An expression used in Maven {@code pom.xml} files, such as <code>${my-property}</code> or
     * <code>my-prefix-${my-property}</code>
     */
    public interface Expression {

        /**
         * A constant containing no <code>${...}</code> placeholders.
         */
        class Constant implements Expression {
            final String expression;

            public Constant(String expression) {
                super();
                this.expression = expression;
            }

            @Override
            public boolean equals(Object obj) {
                if (this == obj)
                    return true;
                if (obj == null)
                    return false;
                if (getClass() != obj.getClass())
                    return false;
                return this.expression.equals(((Constant) obj).expression);
            }

            @Override
            public String evaluate(MavenSourceTree tree, Predicate<Profile> isProfileActive) {
                return expression;
            }

            public String getExpression() {
                return expression;
            }

            @Override
            public String getRawExpression() {
                return expression;
            }

            @Override
            public int hashCode() {
                return expression.hashCode();
            }

            @Override
            public String toString() {
                return expression;
            }

        }

        /**
         * An {@link Expression} containing <code>${...}</code> placeholders.
         */
        class NonConstant extends Constant {

            private static final Pattern PLACE_HOLDER_PATTERN = Pattern.compile("\\$\\{([^\\}]+)\\}");

            static void evaluateExpression(final NonConstant expression, final MavenSourceTree tree,
                    final Predicate<Profile> isProfileActive, final Consumer<ValueDefinition> consumer) {
                final Module context = tree.getModulesByGa().get(expression.ga);
                final String src = expression.expression;
                final Matcher m = PLACE_HOLDER_PATTERN.matcher(src);
                int offset = 0;
                while (m.find()) {
                    if (m.start() > offset) {
                        consumer.accept(new ValueDefinition(context, null,
                                new Expression.Constant(src.substring(offset, m.start()))));
                    }
                    final String propName = m.group(1);
                    evaluateProperty(tree, context, isProfileActive, propName, consumer);
                    offset = m.end();
                }
                if (offset < src.length()) {
                    consumer.accept(new ValueDefinition(context, null,
                            new Expression.Constant(src.substring(offset, src.length()))));
                }
            }

            static void evaluateProperty(final MavenSourceTree tree, final Module context,
                    final Predicate<Profile> isProfileActive, final String propertyName,
                    final Consumer<ValueDefinition> consumer) {

                if ("project.version".equals(propertyName)) {
                    consumer.accept(new ValueDefinition(context, PROJECT_VERSION_XPATH, context.getGav().getVersion()));
                } else if ("project.groupId".equals(propertyName)) {
                    consumer.accept(
                            new ValueDefinition(context, PROJECT_GROUP_ID_XPATH, context.getGav().getGroupId()));
                } else if ("project.artifactId".equals(propertyName)) {
                    consumer.accept(
                            new ValueDefinition(context, PROJECT_ARTIFACT_ID_XPATH, context.getGav().getGroupId()));
                } else {
                    final ValueDefinition propertyDefinition = context.findPropertyDefinition(propertyName,
                            isProfileActive);
                    if (propertyDefinition == null) {
                        /* No such property: climb up */
                        final Module parent = tree.getDeclaredParentModule(context);
                        if (parent == null) {
                            /* unable to resolve */
                            throw new IllegalStateException(String.format(
                                    "Unable to resolve property [%s]: root of the module hierarchy reached",
                                    propertyName));
                        } else {
                            evaluateProperty(tree, parent, isProfileActive, propertyName, consumer);
                        }
                    } else {
                        consumer.accept(propertyDefinition);
                    }

                }
            }

            /**
             * {@link Ga} of the module where the resolution of this {@link Expression} should start. For expressions
             * used in {@code <parent>} block, this would be the parent {@link Ga}. For all other cases, this is the
             * {@link Gav} of the {@code pom.xml} where this {@link Expression} occurs.
             */
            private final Ga ga;

            public NonConstant(String expression, Ga ga) {
                super(expression);
                this.ga = ga;
            }

            @Override
            public boolean equals(Object obj) {
                if (this == obj)
                    return true;
                if (!super.equals(obj))
                    return false;
                if (getClass() != obj.getClass())
                    return false;
                NonConstant other = (NonConstant) obj;
                if (ga == null) {
                    if (other.ga != null)
                        return false;
                } else if (!ga.equals(other.ga))
                    return false;
                return true;
            }

            /** {@inheritDoc} */
            @Override
            public String evaluate(final MavenSourceTree tree, final Predicate<Profile> isProfileActive) {

                final StringBuffer result = new StringBuffer();
                final Consumer<ValueDefinition> consumer = new Consumer<ValueDefinition>() {
                    @Override
                    public void accept(ValueDefinition propertyDefinition) {
                        final Expression propertyValue = propertyDefinition.getValue();
                        if (propertyValue instanceof Constant) {
                            result.append(propertyValue.evaluate(tree, isProfileActive));
                        } else if (propertyValue instanceof NonConstant) {
                            evaluateExpression((NonConstant) propertyValue, tree, isProfileActive, this);
                        } else {
                            throw new IllegalStateException();
                        }
                    }
                };

                evaluateExpression(this, tree, isProfileActive, consumer);
                return result.toString();
            }

            @Override
            public int hashCode() {
                final int prime = 31;
                int result = super.hashCode();
                result = prime * result + ((ga == null) ? 0 : ga.hashCode());
                return result;
            }

        }

        /**
         * @param expression the expression possibly containing <code>${...}</code> placeholders
         * @param ga the {@link Ga} against which the resulting {@link Expression} should be evaluated
         * @return a {@link NonConstant} or {@link Constant} depending on whether the given {@code expression} contains
         *         <code>${</code>
         */
        static Expression of(String expression, Ga ga) {
            if (expression.indexOf("${") >= 0) {
                return new NonConstant(expression, ga);
            } else {
                return new Constant(expression);
            }
        }

        /**
         * Evaluate this {@link Expression} by recursively expanding all ${...} placeholders.
         *
         * @param tree the {@link MavenSourceTree} against which this {@link Expression} should be evaluated
         * @param isProfileActive the profile selector to use when evaluating properties
         * @return the result of evaluation
         */
        String evaluate(MavenSourceTree tree, Predicate<Profile> isProfileActive);

        /**
         * @return the raw source of this {@link Expression}
         */
        String getRawExpression();
    }

    /**
     * A {@link Ga} combined with a version {@link Expression}.
     */
    public static class GavExpression {

        public static class DependencyBuilder extends PlainGavBuilder {

            private String scope = "compile";

            public DependencyBuilder(ModuleGavBuilder module) {
                super(module);
            }

            public Dependency build() {
                final Ga ga = module.getGa();
                return new Dependency(Expression.of(groupId, ga), Expression.of(artifactId, ga),
                        version != null ? Expression.of(version, ga) : null, scope);
            }

            public void scope(String scope) {
                this.scope = scope;
            }

        }

        interface GavBuilder {
            void artifactId(String artifactId);

            void groupId(String groupId);

            void version(String version);

        }

        static class ModuleGavBuilder extends ParentGavBuilder {
            private Ga ga;
            private final ParentGavBuilder parent;

            ModuleGavBuilder(ParentGavBuilder parent) {
                super();
                this.parent = parent;
            }

            public GavExpression build() {
                final Ga ga = getGa();
                final Expression v = version != null ? Expression.of(version, ga) : parent.build().getVersion();
                return new GavExpression(Expression.of(ga.getGroupId(), ga), Expression.of(ga.getArtifactId(), ga), v);
            }

            public Ga getGa() {
                if (this.ga == null) {
                    final String g = groupId != null ? groupId : parent.groupId;
                    this.ga = new Ga(g, artifactId);
                }
                return this.ga;
            }
        }

        static class ParentGavBuilder implements GavBuilder {

            String artifactId;
            String groupId;
            String version;

            @Override
            public void artifactId(String artifactId) {
                this.artifactId = artifactId;
            }

            public GavExpression build() {
                final int sum = (groupId != null ? 1 : 0) + (artifactId != null ? 1 : 0) + (version != null ? 1 : 0);
                switch (sum) {
                case 0:
                    /* none of the three set */
                    return null;
                case 3:
                    final Ga ga = new Ga(groupId, artifactId);
                    return new GavExpression(Expression.of(groupId, ga), Expression.of(artifactId, ga),
                            Expression.of(version, ga));
                default:
                    throw new IllegalStateException(String.format(
                            "groupId, artifactId and version must be all null or both not null: groupId: [%s], artifactId: [%s], version: [%s]",
                            groupId, artifactId, version));
                }
            }

            @Override
            public void groupId(String groupId) {
                this.groupId = groupId;
            }

            @Override
            public void version(String version) {
                this.version = version;
            }
        }

        public static class PlainGavBuilder extends ParentGavBuilder {

            final ModuleGavBuilder module;

            PlainGavBuilder(ModuleGavBuilder module) {
                super();
                this.module = module;
            }

            public GavExpression build() {
                final Ga ga = module.getGa();
                return new GavExpression(Expression.of(groupId, ga), Expression.of(artifactId, ga),
                        version != null ? Expression.of(version, ga) : null);
            }
        }

        public static class PluginGavBuilder extends PlainGavBuilder {

            private List<PlainGavBuilder> dependencies = new ArrayList<>();

            PluginGavBuilder(ModuleGavBuilder module) {
                super(module);
                this.groupId = "org.apache.maven.plugins";
            }

            public Plugin build() {
                final Set<GavExpression> useDependencies = Collections.unmodifiableSet(dependencies.stream()
                        .map(PlainGavBuilder::build).collect(Collectors.toCollection(LinkedHashSet::new)));
                dependencies = null;
                final Ga ga = module.getGa();
                return new Plugin(Expression.of(groupId, ga), Expression.of(artifactId, ga),
                        version != null ? Expression.of(version, ga) : null, useDependencies);
            }

            public void dependency(PlainGavBuilder gav) {
                dependencies.add(gav);
            }

        }

        private final Expression artifactId;
        private volatile Ga ga;
        private final Expression groupId;

        private final Expression version;

        GavExpression(Expression groupId, Expression artifactId, Expression version) {
            SrcdepsCoreUtils.assertArgNotNull(groupId, "groupId");
            SrcdepsCoreUtils.assertArgNotNull(artifactId, "artifactId");
            this.groupId = groupId;
            this.artifactId = artifactId;
            this.version = version;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            GavExpression other = (GavExpression) obj;
            if (artifactId == null) {
                if (other.artifactId != null)
                    return false;
            } else if (!artifactId.equals(other.artifactId))
                return false;
            if (groupId == null) {
                if (other.groupId != null)
                    return false;
            } else if (!groupId.equals(other.groupId))
                return false;
            if (version == null) {
                if (other.version != null)
                    return false;
            } else if (!version.equals(other.version))
                return false;
            return true;
        }

        public Expression getArtifactId() {
            return artifactId;
        }

        public Expression getGroupId() {
            return groupId;
        }

        public Expression getVersion() {
            return version;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((artifactId == null) ? 0 : artifactId.hashCode());
            result = prime * result + ((groupId == null) ? 0 : groupId.hashCode());
            result = prime * result + ((version == null) ? 0 : version.hashCode());
            return result;
        }

        public Gav resolve(final MavenSourceTree tree, Predicate<Profile> isProfileActive) {
            return new Gav(groupId.evaluate(tree, isProfileActive), artifactId.evaluate(tree, isProfileActive),
                    version.evaluate(tree, isProfileActive));
        }

        public Ga resolveGa(final MavenSourceTree tree, Predicate<Profile> isProfileActive) {
            if (ga == null) {
                ga = new Ga(groupId.evaluate(tree, isProfileActive), artifactId.evaluate(tree, isProfileActive));
            }
            return ga;
        }

        @Override
        public String toString() {
            return groupId.getRawExpression() + ":" + artifactId.getRawExpression() + ":" + version;
        }

    }

    /**
     * A Maven module.
     */
    public static class Module {

        /**
         * A {@link Module} builder.
         */
        static class Builder {
            Profile.Builder implicitProfile = new Profile.Builder();
            final ModuleGavBuilder moduleGav;
            final ParentGavBuilder parentGav;
            /** Relative to source tree root directory */
            final String pomPath;
            List<Profile.Builder> profiles;

            Builder(Path rootDirectory, Path pomXml, Charset encoding) {
                parentGav = new ParentGavBuilder();
                moduleGav = new ModuleGavBuilder(parentGav);
                profiles = new ArrayList<>();
                profiles.add(implicitProfile);

                final Stack<String> elementStack = new Stack<>();
                final Path dir = pomXml.getParent();
                try (Reader in = Files.newBufferedReader(pomXml, encoding)) {
                    final XMLEventReader r = xmlInputFactory.createXMLEventReader(in);
                    this.pomPath = SrcdepsCoreUtils.toUnixPath(rootDirectory.relativize(pomXml).toString());

                    final Stack<GavExpression.GavBuilder> gavBuilderStack = new Stack<>();
                    gavBuilderStack.push(moduleGav);
                    Profile.Builder profile = implicitProfile;

                    int ignoredSubtreesCount = 0;

                    while (r.hasNext()) {
                        final XMLEvent e = r.nextEvent();
                        if (e.isStartElement()) {
                            final String elementName = e.asStartElement().getName().getLocalPart();
                            final int elementStackSize = elementStack.size();
                            if (ignoredSubtreesCount > 0) {
                                /* ignore */
                            } else if ("configuration".equals(elementName) || "exclusions".equals(elementName)) {
                                /* ignore elements under <configuration>, etc. */
                                ignoredSubtreesCount++;
                            } else if ("parent".equals(elementName) && r.hasNext()) {
                                gavBuilderStack.push(parentGav);
                            } else if ("dependency".equals(elementName)) {
                                final String grandParent = elementStack.get(elementStackSize - 2);
                                final DependencyBuilder gav = new DependencyBuilder(moduleGav);
                                if ("dependencyManagement".equals(grandParent)) {
                                    profile.dependencyManagement.add(gav);
                                } else if ("project".equals(grandParent) || "profile".equals(grandParent)) {
                                    profile.dependencies.add(gav);
                                } else if ("plugin".equals(grandParent)) {
                                    final PluginGavBuilder pluginGavBuilder = (PluginGavBuilder) gavBuilderStack.peek();
                                    pluginGavBuilder.dependency(gav);
                                } else {
                                    log.warn("srcdeps: Unexpected grand parent of <dependency>: <{}> in [{}]",
                                            grandParent, pomXml);
                                }
                                gavBuilderStack.push(gav);
                            } else if ("extension".equals(elementName)) {
                                final PlainGavBuilder gav = new PlainGavBuilder(moduleGav);
                                profile.extensions.add(gav);
                                gavBuilderStack.push(gav);
                            } else if ("plugin".equals(elementName)) {
                                final PluginGavBuilder gav = new PluginGavBuilder(moduleGav);
                                gavBuilderStack.push(gav);
                                final String parentElement = elementStack.peek();
                                if ("plugins".equals(parentElement) && elementStack.size() > 1
                                        && "pluginManagement".equals(elementStack.get(elementStackSize - 2))) {
                                    profile.pluginManagement.add(gav);
                                } else if ("plugins".equals(parentElement)) {
                                    profile.plugins.add(gav);
                                } else {
                                    throw new IllegalStateException(
                                            String.format("Unexpected grand parent of <plugin>: <%s> in [%s]",
                                                    parentElement, pomXml));
                                }
                            } else if ("module".equals(elementName)) {
                                final String relPath = r.nextEvent().asCharacters().getData() + "/pom.xml";
                                final Path childPomXml = dir.resolve(relPath).normalize();
                                final String rootRelPath = rootDirectory.relativize(childPomXml).toString();
                                profile.children.add(SrcdepsCoreUtils.toUnixPath(rootRelPath));
                            } else if (elementStackSize > 0 && "properties".equals(elementStack.peek())) {
                                final XMLEvent nextEvent = r.peek();
                                if (nextEvent instanceof Characters) {
                                    profile.properties.add(new Profile.PropertyBuilder(elementName,
                                            r.nextEvent().asCharacters().getData(), moduleGav));
                                } else if (nextEvent instanceof EndElement) {
                                    profile.properties.add(new Profile.PropertyBuilder(elementName, "", moduleGav));
                                } else {
                                    throw new IllegalStateException(String.format("Unexpected XML event [%s] in [%s]",
                                            nextEvent.getClass().getName(), pomXml));
                                }
                            } else if ("profile".equals(elementName)) {
                                profile = new Profile.Builder();
                            } else if ("id".equals(elementName)) {
                                if ("profile".equals(elementStack.peek())) {
                                    final String id = r.nextEvent().asCharacters().getData();
                                    profile.id(id);
                                    profiles.add(profile);
                                }
                            } else if ("groupId".equals(elementName)) {
                                gavBuilderStack.peek().groupId(r.nextEvent().asCharacters().getData());
                            } else if ("artifactId".equals(elementName)) {
                                gavBuilderStack.peek().artifactId(r.nextEvent().asCharacters().getData());
                            } else if ("version".equals(elementName)) {
                                gavBuilderStack.peek().version(r.nextEvent().asCharacters().getData());
                            } else if ("scope".equals(elementName)
                                    && gavBuilderStack.peek() instanceof DependencyBuilder) {
                                ((DependencyBuilder) gavBuilderStack.peek())
                                        .scope(r.nextEvent().asCharacters().getData());
                            }
                            elementStack.push(elementName);
                        } else if (e.isEndElement()) {
                            final String elementName = elementStack.pop();
                            if ("configuration".equals(elementName) || "exclusions".equals(elementName)) {
                                /* ignore elements under <configuration>, etc. */
                                ignoredSubtreesCount--;
                            } else if (ignoredSubtreesCount > 0) {
                                /* ignore */
                            } else if ("parent".equals(elementName)) {
                                final GavBuilder gav = gavBuilderStack.pop();
                                assert gav instanceof ParentGavBuilder;
                            } else if ("dependency".equals(elementName)) {
                                final GavBuilder gav = gavBuilderStack.pop();
                                assert gav instanceof PlainGavBuilder;
                            } else if ("extension".equals(elementName)) {
                                final GavBuilder gav = gavBuilderStack.pop();
                                assert gav instanceof PlainGavBuilder;
                            } else if ("plugin".equals(elementName)) {
                                final GavBuilder gav = gavBuilderStack.pop();
                                assert gav instanceof PluginGavBuilder;
                            } else if ("profile".equals(elementName)) {
                                profile = implicitProfile;
                            }
                        }
                    }
                } catch (IOException | XMLStreamException e1) {
                    throw new RuntimeException(e1);
                }
            }

            public Module build() {
                final List<Profile> useProfiles = Collections
                        .unmodifiableList(profiles.stream().map(Profile.Builder::build).collect(Collectors.toList()));
                profiles = null;
                return new Module(pomPath, moduleGav.build(), parentGav.build(), useProfiles);
            }

        }

        /**
         * A Maven profile.
         */
        public static class Profile {

            /**
             * A Maven {@link Profile} builder.
             */
            public static class Builder {
                Set<String> children = new LinkedHashSet<>();
                List<DependencyBuilder> dependencies = new ArrayList<>();
                List<DependencyBuilder> dependencyManagement = new ArrayList<>();
                List<PlainGavBuilder> extensions = new ArrayList<>();
                private String id;
                List<PluginGavBuilder> pluginManagement = new ArrayList<>();
                List<PluginGavBuilder> plugins = new ArrayList<>();
                List<PropertyBuilder> properties = new ArrayList<>();

                public Profile build() {
                    final Set<String> useChildren = Collections.unmodifiableSet(children);
                    children = null;
                    final Set<Dependency> useDependencies = Collections
                            .<Dependency>unmodifiableSet(dependencies.stream().map(DependencyBuilder::build)
                                    .collect(Collectors.toCollection(LinkedHashSet::new)));
                    dependencies = null;
                    final Set<Dependency> useManagedDependencies = Collections
                            .<Dependency>unmodifiableSet(dependencyManagement.stream().map(DependencyBuilder::build)
                                    .collect(Collectors.toCollection(LinkedHashSet::new)));
                    dependencyManagement = null;
                    final Set<Plugin> usePlugins = Collections.<Plugin>unmodifiableSet(plugins.stream()
                            .map(PluginGavBuilder::build).collect(Collectors.toCollection(LinkedHashSet::new)));
                    plugins = null;
                    final Set<Plugin> usePluginManagement = Collections
                            .<Plugin>unmodifiableSet(pluginManagement.stream().map(PluginGavBuilder::build)
                                    .collect(Collectors.toCollection(LinkedHashSet::new)));
                    pluginManagement = null;

                    final Set<GavExpression> useExtensions = Collections.<GavExpression>unmodifiableSet(extensions
                            .stream().map(PlainGavBuilder::build).collect(Collectors.toCollection(LinkedHashSet::new)));
                    extensions = null;

                    final Map<String, Expression> useProps = Collections.unmodifiableMap(properties.stream() //
                            .map(PropertyBuilder::build) //
                            .collect( //
                                    Collectors.toMap( //
                                            e -> e.getKey(), //
                                            e -> e.getValue(), //
                                            (u, v) -> {
                                                throw new IllegalStateException(String.format("Duplicate key %s", u));
                                            }, //
                                            LinkedHashMap::new //
                                    ) //
                            ) //
                    );
                    this.properties = null;
                    return new Profile(id, useChildren, useDependencies, useManagedDependencies, usePlugins,
                            usePluginManagement, useExtensions, useProps);
                }

                public void id(String id) {
                    this.id = id;
                }
            }

            static class PropertyBuilder {
                final ModuleGavBuilder ga;

                final String key;
                final String value;

                PropertyBuilder(String key, String value, ModuleGavBuilder ga) {
                    super();
                    this.key = key;
                    this.value = value;
                    this.ga = ga;
                }

                public Map.Entry<String, Expression> build() {
                    final Expression val = Expression.of(value, ga.getGa());
                    return new AbstractMap.SimpleImmutableEntry<>(key, val);
                }
            }

            /** A path to child project's pom.xml relative to {@link MavenSourceTree#rootDirectory} */
            private final Set<String> children;
            private final Set<Dependency> dependencies;
            private final Set<Dependency> dependencyManagement;
            private final Set<GavExpression> extensions;
            private final String id;
            private final Set<Plugin> pluginManagement;

            private final Set<Plugin> plugins;
            private final Map<String, Expression> properties;

            Profile(String id, Set<String> children, Set<Dependency> dependencies, Set<Dependency> dependencyManagement,
                    Set<Plugin> plugins, Set<Plugin> pluginManagement, Set<GavExpression> extensions,
                    Map<String, Expression> properties) {
                super();
                this.id = id;
                this.children = children;
                this.dependencies = dependencies;
                this.dependencyManagement = dependencyManagement;
                this.plugins = plugins;
                this.pluginManagement = pluginManagement;
                this.extensions = extensions;
                this.properties = properties;
            }

            /**
             * @return a {@link Set} of paths to {@code pom.xml} files of child modules of this Module realtive to
             *         {@link MavenSourceTree#getRootDirectory()}
             */
            public Set<String> getChildren() {
                return children;
            }

            /**
             * @return a {@link Set} of dependencies declared in this {@link Profile}
             */
            public Set<Dependency> getDependencies() {
                return dependencies;
            }

            /**
             * @return a {@link Set} of dependencyManagement entries declared in this {@link Profile}
             */
            public Set<Dependency> getDependencyManagement() {
                return dependencyManagement;
            }

            public Set<GavExpression> getExtensions() {
                return extensions;
            }

            /**
             * @return an ID of this profile or {@code null} if this is the representation of a top level profile-less
             *         dependencies, plugins, etc.
             */
            public String getId() {
                return id;
            }

            /**
             * @return a {@link Set} of pluginManagement entries declared in this {@link Profile}
             */
            public Set<Plugin> getPluginManagement() {
                return pluginManagement;
            }

            /**
             * @return a {@link Set} of plugins declared in this {@link Profile}
             */
            public Set<Plugin> getPlugins() {
                return plugins;
            }

            /**
             * @return the {@link Map} of properties declared in this {@link Profile}
             */
            public Map<String, Expression> getProperties() {
                return properties;
            }

        }

        private final GavExpression gav;
        private final GavExpression parentGav;
        /** Relative to source tree root directory */
        private final String pomPath;

        private final List<Profile> profiles;

        Module(String pomPath, GavExpression gav, GavExpression parentGa, List<Profile> profiles) {
            super();
            this.pomPath = pomPath;
            this.gav = gav;
            this.parentGav = parentGa;
            this.profiles = profiles;
        }

        /**
         * Goes through active profiles and find the definition of the property having the given {@code propertyName}.
         *
         * @param propertyName the property name to find a definition for
         * @param isProfileActive tells which profiles are active
         * @return the {@link ValueDefinition} of the seeked property or {@code null} if no such property is defined in
         *         this {@link Module}
         */
        public ValueDefinition findPropertyDefinition(String propertyName, Predicate<Profile> isProfileActive) {
            final ListIterator<Profile> it = this.profiles.listIterator(this.profiles.size());
            while (it.hasPrevious()) {
                final Profile p = it.previous();
                if (isProfileActive.test(p)) {
                    final Expression result = p.properties.get(propertyName);
                    if (result != null) {
                        final String xPath = xPathProfile(p.getId(), "properties", propertyName);
                        return new ValueDefinition(this, xPath, result);
                    }
                }
            }
            return null;
        }

        /**
         * @return the {@link GavExpression} of this Maven module
         */
        public GavExpression getGav() {
            return gav;
        }

        /**
         * @return the {@link GavExpression} of the Maven parent module of this module or {@code null} if this module
         *         has no parent
         */
        public GavExpression getParentGav() {
            return parentGav;
        }

        /**
         * @return a path to the this module's {@code pom.xml} relative to {@link MavenSourceTree#getRootModule()}
         */
        public String getPomPath() {
            return pomPath;
        }

        /**
         * @return the {@link List} of profiles defined in this {@link Module}. Note that the top level profile-less
         *         dependencies, dependencyManagement, etc. are defined in {@link Module} with {@code id} {@code null}.
         */
        public List<Profile> getProfiles() {
            return profiles;
        }

        /**
         * @param childPomPath a path to {@code pom.xml} file relative to {@link MavenSourceTree#rootDirectory}
         * @param isProfileActive
         * @return {@code true} if the {@code pom.xml} represented by this {@link Module} has a {@code <module>} with
         *         the given pom.xml path or {@code false} otherwise
         */
        public boolean hasChild(String childPomPath, Predicate<Profile> isProfileActive) {
            for (Profile p : profiles) {
                if (isProfileActive.test(p)) {
                    if (p.children.contains(childPomPath)) {
                        return true;
                    }
                }
            }
            return false;
        }

    }

    public static class Plugin extends GavExpression {
        private final Set<GavExpression> dependencies;

        public Plugin(Expression groupId, Expression artifactId, Expression version, Set<GavExpression> dependencies) {
            super(groupId, artifactId, version);
            this.dependencies = dependencies;
        }

        public Set<GavExpression> getDependencies() {
            return dependencies;
        }
    }

    /**
     * Decides which {@link ValueDefinition}s delivered via {@link SimplePlaceHolderConsumer#accept(ValueDefinition)}
     * are relevant for setting a new version and eventually adds a new {@link DomEdit} operation to
     * {@link SimplePlaceHolderConsumer#edits}.
     */
    static class SimplePlaceHolderConsumer implements Consumer<ValueDefinition> {

        int counter = 0;

        final DomEdits edits;
        final String newValue;

        SimplePlaceHolderConsumer(DomEdits edits, String newValue) {
            super();
            this.edits = edits;
            this.newValue = newValue;
        }

        @Override
        public void accept(ValueDefinition valueDefinition) {
            if (counter++ > 0) {
                throw new IllegalStateException(String.format("Cannot call [%s] more than once",
                        SimplePlaceHolderConsumer.class.getSimpleName()));
            }
            if (valueDefinition.getXPath() == null) {
                throw new IllegalStateException(String.format("[%s] cannot accept a value without an xPath",
                        SimplePlaceHolderConsumer.class.getSimpleName()));
            } else if (PROJECT_VERSION_XPATH.equals(valueDefinition.getXPath())) {
                /* ignore */
            } else {
                edits.add(valueDefinition.getModule().getPomPath(), new DomEdit(valueDefinition.getXPath(), newValue));
            }
        }

    }

    /**
     * Where some {@link Expression}'s value is defined - a semi-result of an evaluation of some {@link Expression}.
     */
    static class ValueDefinition {
        private final Module module;
        private final Expression value;
        private final String xPath;

        ValueDefinition(Module module, String xPath, Expression value) {
            super();
            this.module = module;
            this.xPath = xPath;
            this.value = value;
        }

        /**
         * @return the {@link Module} in which {@link #value} is defined
         */
        public Module getModule() {
            return module;
        }

        /**
         * @return the value of the {@link Expression}
         */
        public Expression getValue() {
            return value;
        }

        /**
         * @return an XPath expression pointing at the element where the {@link #value} is defined
         */
        public String getXPath() {
            return xPath;
        }
    }

    private static final Logger log = LoggerFactory.getLogger(MavenSourceTree.class);

    private static final String PROJECT_ARTIFACT_ID_XPATH = "/*[local-name()='project']/*[local-name()='artifactId']";
    private static final String PROJECT_GROUP_ID_XPATH = "/*[local-name()='project']/*[local-name()='groupId']";
    private static final String PROJECT_VERSION_XPATH = "/*[local-name()='project']/*[local-name()='version']";

    private static final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();

    /**
     * @param rootPomXml the path to the {@code pom.xml} file of the root Maven module
     * @param encoding the encoding to use when reading {@code pom.xml} files in the given file tree
     * @return a new {@link MavenSourceTree}
     */
    public static MavenSourceTree of(Path rootPomXml, Charset encoding) {
        return new Builder(rootPomXml.getParent(), encoding).pomXml(rootPomXml).build();
    }

    static Node readDom(Transformer transformer, final Path pomXml, final Charset encoding) {
        try (Reader in = Files.newBufferedReader(pomXml, encoding)) {
            final DOMResult result = new DOMResult();
            transformer.transform(new StreamSource(in), result);
            return result.getNode();
        } catch (IOException | TransformerException | TransformerFactoryConfigurationError e) {
            throw new RuntimeException(String.format("Could not read DOM from [%s]", pomXml), e);
        }
    }

    static void writeDom(Transformer transformer, Path pomXml, final Node pomXmlDocument, Charset encoding) {
        try (Writer out = Files.newBufferedWriter(pomXml, encoding)) {
            transformer.transform(new DOMSource(pomXmlDocument), new StreamResult(out));
        } catch (IOException | TransformerException | TransformerFactoryConfigurationError e) {
            throw new RuntimeException(String.format("Could not write DOM to [%s]", pomXml), e);
        }
    }

    static String xPath(String... elements) {
        final StringBuilder result = new StringBuilder();
        for (String e : elements) {
            result.append("/*[local-name()='").append(e).append("']");
        }
        return result.toString();
    }

    static String xPathDependency(String dependencyKind, GavExpression gav) {
        return "/*[local-name()='" + dependencyKind + "' and *[local-name()='groupId' and text()='"
                + gav.getGroupId().getRawExpression() + "'] and *[local-name()='artifactId' and text()='"
                + gav.getArtifactId().getRawExpression() + "']]";
    }

    static String xPathDependencyVersion(String dependencyKind, GavExpression gav) {
        return xPathDependency(dependencyKind, gav) + "/*[local-name()='version']";
    }

    static String xPathProfile(String id, String... elements) {
        return "/*[local-name()='project']" + (id == null ? ""
                : "/*[local-name()='profiles']/*[local-name()='profile' and *[local-name()='id' and text()='" + id
                        + "']]")
                + xPath(elements);
    }

    private final Charset encoding;

    private final Map<Ga, Module> modulesByGa;

    private final Map<String, Module> modulesByPath;

    private final Path rootDirectory;

    MavenSourceTree(Path rootDirectory, Charset encoding, Map<String, Module> modulesByPath,
            Map<Ga, Module> modulesByGa) {
        this.rootDirectory = rootDirectory;
        this.modulesByPath = modulesByPath;
        this.modulesByGa = modulesByGa;
        this.encoding = encoding;
    }

    private void addDeclaredParents(final Module module, final Set<Ga> result, Set<Ga> visited,
            Predicate<Profile> isProfileActive) {
        Module parent;
        Module child = module;
        while ((parent = getDeclaredParentModule(child)) != null) {
            addModule(parent.getGav().resolveGa(this, isProfileActive), result, visited, isProfileActive);
            child = parent;
        }
    }

    private void addModule(Ga includeGa, Set<Ga> result, Set<Ga> visited, Predicate<Profile> isProfileActive) {
        final Module module = modulesByGa.get(includeGa);
        if (module != null && !visited.contains(includeGa)) {
            visited.add(includeGa);
            result.add(includeGa);
            addProperParents(module, result, visited, isProfileActive);
            addDeclaredParents(module, result, visited, isProfileActive);
            for (Profile p : module.profiles) {
                if (isProfileActive.test(p)) {
                    for (GavExpression depGa : p.dependencies) {
                        addModule(depGa.resolveGa(this, isProfileActive), result, visited, isProfileActive);
                    }
                    for (GavExpression depGa : p.plugins) {
                        addModule(depGa.resolveGa(this, isProfileActive), result, visited, isProfileActive);
                    }
                }
            }
        }
    }

    private void addProperParents(final Module module, final Set<Ga> result, Set<Ga> visited,
            Predicate<Profile> isProfileActive) {
        Module parent;
        Module child = module;
        while ((parent = getProperParentModule(child, isProfileActive)) != null) {
            addModule(parent.getGav().resolveGa(this, isProfileActive), result, visited, isProfileActive);
            child = parent;
        }
    }

    /**
     * Returns a {@link Set} that contains all given {@code initialModules} and all such modules from the current
     * {@link MavenSourceTree} that are reachable from the {@code initialModules} via <i>depends on</i> and <i>is parent
     * of</i> relationships.
     *
     * @param initialModules
     * @return {@link Set} of {@code groupId:artifactId}
     */
    public Set<Ga> computeModuleClosure(Collection<Ga> initialModules, Predicate<Profile> isProfileActive) {
        final Set<Ga> visited = new HashSet<>();
        final Set<Ga> result = new LinkedHashSet<>();
        for (Ga includeGa : initialModules) {
            addModule(includeGa, result, visited, isProfileActive);
        }
        return result;
    }

    void edit(final String newVersion, final Predicate<Profile> isProfileActive, final DomEdits edits, Module module,
            final Expression moduleVersion, String xPath) {
        if (moduleVersion instanceof NonConstant) {
            NonConstant.evaluateExpression((NonConstant) moduleVersion, this, isProfileActive,
                    new SimplePlaceHolderConsumer(edits, newVersion));
        } else if (moduleVersion instanceof Constant) {
            edits.add(module.getPomPath(), new DomEdit(xPath, newVersion));
        }
    }

    void editDependencies(String newVersion, final Predicate<Profile> isProfileActive, final DomEdits edits,
            Module module, String profileId, Set<Dependency> dependencies, String... path) {
        if (!dependencies.isEmpty()) {
            final String xPathProfile = xPathProfile(profileId, path);
            for (GavExpression gav : dependencies) {
                if (gav.getVersion() != null && modulesByGa.containsKey(gav.resolveGa(this, isProfileActive))) {
                    final String xPath = xPathProfile + xPathDependencyVersion("dependency", gav);
                    edit(newVersion, isProfileActive, edits, module, gav.getVersion(), xPath);
                }
            }
        }
    }

    void editExtensions(String newVersion, final Predicate<Profile> isProfileActive, final DomEdits edits,
            Module module, String profileId, Set<GavExpression> dependencies, String... path) {
        if (!dependencies.isEmpty()) {
            final String xPathProfile = xPathProfile(profileId, path);
            for (GavExpression gav : dependencies) {
                if (gav.getVersion() != null && modulesByGa.containsKey(gav.resolveGa(this, isProfileActive))) {
                    final String xPath = xPathProfile + xPathDependencyVersion("extension", gav);
                    edit(newVersion, isProfileActive, edits, module, gav.getVersion(), xPath);
                }
            }
        }
    }

    void editPlugins(String newVersion, final Predicate<Profile> isProfileActive, final DomEdits edits, Module module,
            String profileId, Set<Plugin> dependencies, String... path) {
        if (!dependencies.isEmpty()) {
            final String xPathProfile = xPathProfile(profileId, path);
            for (Plugin pluginGav : dependencies) {
                final boolean editPlugin = pluginGav.getVersion() != null
                        && modulesByGa.containsKey(pluginGav.resolveGa(this, isProfileActive));
                if (editPlugin || !pluginGav.getDependencies().isEmpty()) {
                    if (editPlugin) {
                        final String xPath = xPathProfile + xPathDependencyVersion("plugin", pluginGav);
                        edit(newVersion, isProfileActive, edits, module, pluginGav.getVersion(), xPath);
                    }
                    if (!pluginGav.getDependencies().isEmpty()) {
                        final String prefix = xPathProfile + xPathDependency("plugin", pluginGav)
                                + xPath("dependencies");
                        for (GavExpression dep : pluginGav.getDependencies()) {
                            if (dep.getVersion() != null
                                    && modulesByGa.containsKey(dep.resolveGa(this, isProfileActive))) {
                                final String xPath = prefix + xPathDependencyVersion("dependency", dep);
                                edit(newVersion, isProfileActive, edits, module, dep.getVersion(), xPath);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Evaluate the given {@link Expression} by recursively expanding all its ${...} placeholders.
     *
     * @param expression the {@link Expression} to evaluate
     * @param isProfileActive the profile selector to use when evaluating properties
     * @return the result of evaluation
     */
    public String evaluate(Expression expression, Predicate<Profile> isProfileActive) {
        return expression.evaluate(this, isProfileActive);
    }

    /**
     * Goes through dependencies and plugins this source tree requires and returns the set of those satisfying
     * {@link GavSet#contains(String, String)}.
     *
     * @param gavSet
     * @param isProfileActive
     * @return a
     */
    public Set<Ga> filterDependencies(GavSet gavSet, final Predicate<Profile> isProfileActive) {
        final Set<Ga> result = new TreeSet<>();
        for (Module module : getModulesByGa().values()) {
            final GavExpression parentGav = module.getParentGav();
            if (parentGav != null) {
                final Ga ga = parentGav.resolveGa(this, isProfileActive);
                if (gavSet.contains(ga.getGroupId(), ga.getArtifactId())) {
                    result.add(ga);
                }
            }
            for (Profile p : module.getProfiles()) {
                if (isProfileActive.test(p)) {
                    for (Dependency depGa : p.getDependencies()) {
                        final Ga ga = depGa.resolveGa(this, isProfileActive);
                        if (gavSet.contains(ga.getGroupId(), ga.getArtifactId())) {
                            result.add(ga);
                        }
                    }
                    for (Dependency depGa : p.getDependencyManagement()) {
                        final Ga ga = depGa.resolveGa(this, isProfileActive);
                        if ("import".equals(depGa.getScope()) && gavSet.contains(ga.getGroupId(), ga.getArtifactId())) {
                            result.add(ga);
                        }
                    }
                    for (Plugin plugin : p.getPlugins()) {
                        {
                            final Ga ga = plugin.resolveGa(this, isProfileActive);
                            if (gavSet.contains(ga.getGroupId(), ga.getArtifactId())) {
                                result.add(ga);
                            }
                        }
                        for (GavExpression plugnDep : plugin.getDependencies()) {
                            final Ga plugnDepGa = plugnDep.resolveGa(this, isProfileActive);
                            if (gavSet.contains(plugnDepGa.getGroupId(), plugnDepGa.getArtifactId())) {
                                result.add(plugnDepGa);
                            }
                        }
                    }
                    for (Plugin plugin : p.getPluginManagement()) {
                        {
                            final Ga ga = plugin.resolveGa(this, isProfileActive);
                            if (gavSet.contains(ga.getGroupId(), ga.getArtifactId())) {
                                result.add(ga);
                            }
                        }
                        for (GavExpression pluginDep : plugin.getDependencies()) {
                            final Ga plugnDepGa = pluginDep.resolveGa(this, isProfileActive);
                            if (gavSet.contains(plugnDepGa.getGroupId(), plugnDepGa.getArtifactId())) {
                                result.add(plugnDepGa);
                            }
                        }
                    }
                    for (GavExpression ext : p.getExtensions()) {
                        final Ga ga = ext.resolveGa(this, isProfileActive);
                        if (gavSet.contains(ga.getGroupId(), ga.getArtifactId())) {
                            result.add(ga);
                        }
                    }
                }
            }
        }
        return result;
    }

    Module getDeclaredParentModule(Module child) {
        final GavExpression parentGa = child.parentGav;
        if (parentGa != null) {
            return modulesByGa.get(parentGa.resolveGa(this, null));
        } else {
            return null;
        }
    }

    /**
     * @return a {@link Charset} to use when reading and writing the {@code pom.xml} files in this
     *         {@link MavenSourceTree}
     */
    public Charset getEncoding() {
        return encoding;
    }

    /**
     * @return a {@link Map} of modules in this {@link MavenSourceTree} by their {@code groupId:artifactId}
     */
    public Map<Ga, Module> getModulesByGa() {
        return modulesByGa;
    }

    /**
     * @return a {@link Map} of modules in this {@link MavenSourceTree} by their {@code pom.xml} path realtive to
     *         {@link #getRootDirectory()}
     */
    public Map<String, Module> getModulesByPath() {
        return modulesByPath;
    }

    /**
     * @param child
     * @return the {@link Module} having the given gild in its {@code <modules>}
     */
    Module getProperParentModule(Module child, Predicate<Profile> isProfileActive) {
        final GavExpression parentGa = child.parentGav;
        if (parentGa != null) {
            final Module declaredParent = modulesByGa.get(parentGa.resolveGa(this, isProfileActive));
            if (declaredParent != null && declaredParent.hasChild(child.pomPath, isProfileActive)) {
                return declaredParent;
            }
            return modulesByGa.values().stream().filter(m -> m.hasChild(child.pomPath, isProfileActive)).findFirst()
                    .orElse(null);
        }
        return null;
    }

    /**
     * @return the root directory of this {@link MavenSourceTree}
     */
    public Path getRootDirectory() {
        return rootDirectory;
    }

    /**
     * @return the module in the directory returned by {@link #getRootDirectory()}
     */
    public Module getRootModule() {
        return modulesByPath.get("pom.xml");
    }

    /**
     * A fast alternative to {@code mvn versions:set -DnewVersion=...}
     *
     * @param newVersion the new version to set
     * @param isProfileActive a {@link Profile} filter, see {@link #profiles(String...)}
     */
    public void setVersions(final String newVersion, final Predicate<Profile> isProfileActive) {
        final DomEdits edits = new DomEdits();
        for (Module module : modulesByGa.values()) {

            /* self */
            final GavExpression parentGav = module.getParentGav();
            final Expression moduleVersion = module.getGav().getVersion();
            if (parentGav == null || !moduleVersion.equals(module.getParentGav().getVersion())) {
                /* explicitly defined version */
                edit(newVersion, isProfileActive, edits, module, moduleVersion, xPath("project", "version"));
            }

            /* parent */
            if (parentGav != null && modulesByGa.containsKey(parentGav.resolveGa(this, isProfileActive))) {
                final Expression parentVersion = parentGav.getVersion();
                edit(newVersion, isProfileActive, edits, module, parentVersion, xPath("project", "parent", "version"));
            }

            for (Profile profile : module.getProfiles()) {
                /* dependencyManagement */
                final String profileId = profile.getId();
                editDependencies(newVersion, isProfileActive, edits, module, profileId,
                        profile.getDependencyManagement(), "dependencyManagement", "dependencies");
                editDependencies(newVersion, isProfileActive, edits, module, profileId, profile.getDependencies(),
                        "dependencies");
                editPlugins(newVersion, isProfileActive, edits, module, profileId, profile.getPluginManagement(),
                        "build", "pluginManagement", "plugins");
                editPlugins(newVersion, isProfileActive, edits, module, profileId, profile.getPlugins(), "build",
                        "plugins");

                editExtensions(newVersion, isProfileActive, edits, module, profileId, profile.getExtensions(), "build",
                        "extensions");

            }
        }
        edits.perform(rootDirectory, encoding);
    }

    Map<String, Set<String>> unlinkUneededModules(Set<Ga> includes, Module module,
            Map<String, Set<String>> removeChildPaths, Predicate<Profile> isProfileActive) {
        for (Profile p : module.profiles) {
            if (isProfileActive.test(p)) {
                for (String childPath : p.children) {
                    final Module childModule = modulesByPath.get(childPath);
                    final GavExpression childGa = childModule.gav;
                    if (!includes.contains(childGa.resolveGa(this, isProfileActive))) {
                        Set<String> set = removeChildPaths.get(module.pomPath);
                        if (set == null) {
                            set = new LinkedHashSet<String>();
                            removeChildPaths.put(module.pomPath, set);
                        }
                        set.add(childPath);
                    } else {
                        unlinkUneededModules(includes, childModule, removeChildPaths, isProfileActive);
                    }
                }
            }
        }
        return removeChildPaths;
    }

    /**
     * Edit the {@code pom.xml} files so that just the given @{@code includes} are buildable, removing all unnecessary
     * {@code <module>} elements from {@code pom.xml} files.
     *
     * @param includes a list of {@code groupId:artifactId}s
     * @param isProfileActive a {@link Profile} filter, see {@link #profiles(String...)}
     */
    public void unlinkUneededModules(Set<Ga> includes, Predicate<Profile> isProfileActive) {
        final Module rootModule = modulesByPath.get("pom.xml");
        final Map<String, Set<String>> removeChildPaths = unlinkUneededModules(includes, rootModule,
                new LinkedHashMap<String, Set<String>>(), isProfileActive);
        Transformer transformer = null;
        XPathFactory xPathFactory = null;
        for (Entry<String, Set<String>> e : removeChildPaths.entrySet()) {
            if (transformer == null) {
                try {
                    transformer = TransformerFactory.newInstance().newTransformer();
                } catch (TransformerConfigurationException | TransformerFactoryConfigurationError e1) {
                    throw new RuntimeException(e1);
                }
                xPathFactory = XPathFactory.newInstance();
            }
            unlinkUneededModules(transformer, xPathFactory, rootDirectory.resolve(e.getKey()), e.getValue());
        }
    }

    void unlinkUneededModules(Transformer transformer, XPathFactory xPathFactory, Path pomXml,
            Set<String> removeChildPaths) {
        final Node pomXmlDocument = readDom(transformer, pomXml, encoding);
        final XPath xPath = xPathFactory.newXPath();
        try {
            final NodeList moduleNodes = (NodeList) xPath.evaluate("//*[local-name()='module']", pomXmlDocument,
                    XPathConstants.NODESET);
            final Path dir = pomXml.getParent();
            for (int i = 0; i < moduleNodes.getLength(); i++) {
                final Node moduleNode = moduleNodes.item(i);
                final String moduleText = moduleNode.getTextContent();
                final Path childPath = dir.resolve(moduleText + "/pom.xml").normalize();
                final String rootRelChildPath = SrcdepsCoreUtils
                        .toUnixPath(rootDirectory.relativize(childPath).toString());
                if (removeChildPaths.contains(rootRelChildPath)) {
                    final Node parent = moduleNode.getParentNode();
                    final Comment moduleComment = moduleNode.getOwnerDocument()
                            .createComment(" <module>" + moduleText + "</module> removed by srcdeps ");
                    parent.replaceChild(moduleComment, moduleNode);
                }
            }
            writeDom(transformer, pomXml, pomXmlDocument, encoding);
        } catch (XPathExpressionException | DOMException e) {
            throw new RuntimeException(String.format("Could not transform [%s]", pomXml), e);
        }
    }

} public static class ActiveProfiles implements Predicate<Profile> {

    static final Predicate<Profile> EMPTY = new ActiveProfiles();

    /**
     * @param profileIds the active profiles (can be empty)
     * @return a new {@link Profile} filter which will hold the named {@code profileIds} for active
     */
    public static Predicate<Profile> of(String... profileIds) {
        return profileIds.length == 0 ? EMPTY : new ActiveProfiles(profileIds);
    }

    /**
     * @param args Maven command line arguments
     * @return a new {@link Predicate}
     */
    public static Predicate<Profile> ofArgs(List<String> args) {
        for (Iterator<String> it = args.iterator(); it.hasNext();) {
            final String arg = it.next();
            if ("-P".equals(arg) || "--activate-profiles".equals(arg)) {
                return of(it.next().split(","));
            } else if (arg.startsWith("-P")) {
                return of(arg.substring(2).split(","));
            }
        }
        return EMPTY;
    }

    final Set<String> profileIds;

    ActiveProfiles(String... profileIds) {
        super();
        Set<String> m = new LinkedHashSet<>(profileIds.length);
        for (String profileId : profileIds) {
            m.add(profileId);
        }
        this.profileIds = m;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        ActiveProfiles other = (ActiveProfiles) obj;
        if (profileIds == null) {
            if (other.profileIds != null)
                return false;
        } else if (!profileIds.equals(other.profileIds))
            return false;
        return true;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((profileIds == null) ? 0 : profileIds.hashCode());
        return result;
    }

    @Override
    public boolean test(Profile t) {
        return t.getId() == null || profileIds.contains(t.getId());
    }

} public static class Module {

    /**
     * A {@link Module} builder.
     */
    static class Builder {
        Profile.Builder implicitProfile = new Profile.Builder();
        final ModuleGavBuilder moduleGav;
        final ParentGavBuilder parentGav;
        /** Relative to source tree root directory */
        final String pomPath;
        List<Profile.Builder> profiles;

        Builder(Path rootDirectory, Path pomXml, Charset encoding) {
            parentGav = new ParentGavBuilder();
            moduleGav = new ModuleGavBuilder(parentGav);
            profiles = new ArrayList<>();
            profiles.add(implicitProfile);

            final Stack<String> elementStack = new Stack<>();
            final Path dir = pomXml.getParent();
            try (Reader in = Files.newBufferedReader(pomXml, encoding)) {
                final XMLEventReader r = xmlInputFactory.createXMLEventReader(in);
                this.pomPath = SrcdepsCoreUtils.toUnixPath(rootDirectory.relativize(pomXml).toString());

                final Stack<GavExpression.GavBuilder> gavBuilderStack = new Stack<>();
                gavBuilderStack.push(moduleGav);
                Profile.Builder profile = implicitProfile;

                int ignoredSubtreesCount = 0;

                while (r.hasNext()) {
                    final XMLEvent e = r.nextEvent();
                    if (e.isStartElement()) {
                        final String elementName = e.asStartElement().getName().getLocalPart();
                        final int elementStackSize = elementStack.size();
                        if (ignoredSubtreesCount > 0) {
                            /* ignore */
                        } else if ("configuration".equals(elementName) || "exclusions".equals(elementName)) {
                            /* ignore elements under <configuration>, etc. */
                            ignoredSubtreesCount++;
                        } else if ("parent".equals(elementName) && r.hasNext()) {
                            gavBuilderStack.push(parentGav);
                        } else if ("dependency".equals(elementName)) {
                            final String grandParent = elementStack.get(elementStackSize - 2);
                            final DependencyBuilder gav = new DependencyBuilder(moduleGav);
                            if ("dependencyManagement".equals(grandParent)) {
                                profile.dependencyManagement.add(gav);
                            } else if ("project".equals(grandParent) || "profile".equals(grandParent)) {
                                profile.dependencies.add(gav);
                            } else if ("plugin".equals(grandParent)) {
                                final PluginGavBuilder pluginGavBuilder = (PluginGavBuilder) gavBuilderStack.peek();
                                pluginGavBuilder.dependency(gav);
                            } else {
                                log.warn("srcdeps: Unexpected grand parent of <dependency>: <{}> in [{}]",
                                        grandParent, pomXml);
                            }
                            gavBuilderStack.push(gav);
                        } else if ("extension".equals(elementName)) {
                            final PlainGavBuilder gav = new PlainGavBuilder(moduleGav);
                            profile.extensions.add(gav);
                            gavBuilderStack.push(gav);
                        } else if ("plugin".equals(elementName)) {
                            final PluginGavBuilder gav = new PluginGavBuilder(moduleGav);
                            gavBuilderStack.push(gav);
                            final String parentElement = elementStack.peek();
                            if ("plugins".equals(parentElement) && elementStack.size() > 1
                                    && "pluginManagement".equals(elementStack.get(elementStackSize - 2))) {
                                profile.pluginManagement.add(gav);
                            } else if ("plugins".equals(parentElement)) {
                                profile.plugins.add(gav);
                            } else {
                                throw new IllegalStateException(
                                        String.format("Unexpected grand parent of <plugin>: <%s> in [%s]",
                                                parentElement, pomXml));
                            }
                        } else if ("module".equals(elementName)) {
                            final String relPath = r.nextEvent().asCharacters().getData() + "/pom.xml";
                            final Path childPomXml = dir.resolve(relPath).normalize();
                            final String rootRelPath = rootDirectory.relativize(childPomXml).toString();
                            profile.children.add(SrcdepsCoreUtils.toUnixPath(rootRelPath));
                        } else if (elementStackSize > 0 && "properties".equals(elementStack.peek())) {
                            final XMLEvent nextEvent = r.peek();
                            if (nextEvent instanceof Characters) {
                                profile.properties.add(new Profile.PropertyBuilder(elementName,
                                        r.nextEvent().asCharacters().getData(), moduleGav));
                            } else if (nextEvent instanceof EndElement) {
                                profile.properties.add(new Profile.PropertyBuilder(elementName, "", moduleGav));
                            } else {
                                throw new IllegalStateException(String.format("Unexpected XML event [%s] in [%s]",
                                        nextEvent.getClass().getName(), pomXml));
                            }
                        } else if ("profile".equals(elementName)) {
                            profile = new Profile.Builder();
                        } else if ("id".equals(elementName)) {
                            if ("profile".equals(elementStack.peek())) {
                                final String id = r.nextEvent().asCharacters().getData();
                                profile.id(id);
                                profiles.add(profile);
                            }
                        } else if ("groupId".equals(elementName)) {
                            gavBuilderStack.peek().groupId(r.nextEvent().asCharacters().getData());
                        } else if ("artifactId".equals(elementName)) {
                            gavBuilderStack.peek().artifactId(r.nextEvent().asCharacters().getData());
                        } else if ("version".equals(elementName)) {
                            gavBuilderStack.peek().version(r.nextEvent().asCharacters().getData());
                        } else if ("scope".equals(elementName)
                                && gavBuilderStack.peek() instanceof DependencyBuilder) {
                            ((DependencyBuilder) gavBuilderStack.peek())
                                    .scope(r.nextEvent().asCharacters().getData());
                        }
                        elementStack.push(elementName);
                    } else if (e.isEndElement()) {
                        final String elementName = elementStack.pop();
                        if ("configuration".equals(elementName) || "exclusions".equals(elementName)) {
                            /* ignore elements under <configuration>, etc. */
                            ignoredSubtreesCount--;
                        } else if (ignoredSubtreesCount > 0) {
                            /* ignore */
                        } else if ("parent".equals(elementName)) {
                            final GavBuilder gav = gavBuilderStack.pop();
                            assert gav instanceof ParentGavBuilder;
                        } else if ("dependency".equals(elementName)) {
                            final GavBuilder gav = gavBuilderStack.pop();
                            assert gav instanceof PlainGavBuilder;
                        } else if ("extension".equals(elementName)) {
                            final GavBuilder gav = gavBuilderStack.pop();
                            assert gav instanceof PlainGavBuilder;
                        } else if ("plugin".equals(elementName)) {
                            final GavBuilder gav = gavBuilderStack.pop();
                            assert gav instanceof PluginGavBuilder;
                        } else if ("profile".equals(elementName)) {
                            profile = implicitProfile;
                        }
                    }
                }
            } catch (IOException | XMLStreamException e1) {
                throw new RuntimeException(e1);
            }
        }

        public Module build() {
            final List<Profile> useProfiles = Collections
                    .unmodifiableList(profiles.stream().map(Profile.Builder::build).collect(Collectors.toList()));
            profiles = null;
            return new Module(pomPath, moduleGav.build(), parentGav.build(), useProfiles);
        }

    }

    /**
     * A Maven profile.
     */
    public static class Profile {

        /**
         * A Maven {@link Profile} builder.
         */
        public static class Builder {
            Set<String> children = new LinkedHashSet<>();
            List<DependencyBuilder> dependencies = new ArrayList<>();
            List<DependencyBuilder> dependencyManagement = new ArrayList<>();
            List<PlainGavBuilder> extensions = new ArrayList<>();
            private String id;
            List<PluginGavBuilder> pluginManagement = new ArrayList<>();
            List<PluginGavBuilder> plugins = new ArrayList<>();
            List<PropertyBuilder> properties = new ArrayList<>();

            public Profile build() {
                final Set<String> useChildren = Collections.unmodifiableSet(children);
                children = null;
                final Set<Dependency> useDependencies = Collections
                        .<Dependency>unmodifiableSet(dependencies.stream().map(DependencyBuilder::build)
                                .collect(Collectors.toCollection(LinkedHashSet::new)));
                dependencies = null;
                final Set<Dependency> useManagedDependencies = Collections
                        .<Dependency>unmodifiableSet(dependencyManagement.stream().map(DependencyBuilder::build)
                                .collect(Collectors.toCollection(LinkedHashSet::new)));
                dependencyManagement = null;
                final Set<Plugin> usePlugins = Collections.<Plugin>unmodifiableSet(plugins.stream()
                        .map(PluginGavBuilder::build).collect(Collectors.toCollection(LinkedHashSet::new)));
                plugins = null;
                final Set<Plugin> usePluginManagement = Collections
                        .<Plugin>unmodifiableSet(pluginManagement.stream().map(PluginGavBuilder::build)
                                .collect(Collectors.toCollection(LinkedHashSet::new)));
                pluginManagement = null;

                final Set<GavExpression> useExtensions = Collections.<GavExpression>unmodifiableSet(extensions
                        .stream().map(PlainGavBuilder::build).collect(Collectors.toCollection(LinkedHashSet::new)));
                extensions = null;

                final Map<String, Expression> useProps = Collections.unmodifiableMap(properties.stream() //
                        .map(PropertyBuilder::build) //
                        .collect( //
                                Collectors.toMap( //
                                        e -> e.getKey(), //
                                        e -> e.getValue(), //
                                        (u, v) -> {
                                            throw new IllegalStateException(String.format("Duplicate key %s", u));
                                        }, //
                                        LinkedHashMap::new //
                                ) //
                        ) //
                );
                this.properties = null;
                return new Profile(id, useChildren, useDependencies, useManagedDependencies, usePlugins,
                        usePluginManagement, useExtensions, useProps);
            }

            public void id(String id) {
                this.id = id;
            }
        }

        static class PropertyBuilder {
            final ModuleGavBuilder ga;

            final String key;
            final String value;

            PropertyBuilder(String key, String value, ModuleGavBuilder ga) {
                super();
                this.key = key;
                this.value = value;
                this.ga = ga;
            }

            public Map.Entry<String, Expression> build() {
                final Expression val = Expression.of(value, ga.getGa());
                return new AbstractMap.SimpleImmutableEntry<>(key, val);
            }
        }

        /** A path to child project's pom.xml relative to {@link MavenSourceTree#rootDirectory} */
        private final Set<String> children;
        private final Set<Dependency> dependencies;
        private final Set<Dependency> dependencyManagement;
        private final Set<GavExpression> extensions;
        private final String id;
        private final Set<Plugin> pluginManagement;

        private final Set<Plugin> plugins;
        private final Map<String, Expression> properties;

        Profile(String id, Set<String> children, Set<Dependency> dependencies, Set<Dependency> dependencyManagement,
                Set<Plugin> plugins, Set<Plugin> pluginManagement, Set<GavExpression> extensions,
                Map<String, Expression> properties) {
            super();
            this.id = id;
            this.children = children;
            this.dependencies = dependencies;
            this.dependencyManagement = dependencyManagement;
            this.plugins = plugins;
            this.pluginManagement = pluginManagement;
            this.extensions = extensions;
            this.properties = properties;
        }

        /**
         * @return a {@link Set} of paths to {@code pom.xml} files of child modules of this Module realtive to
         *         {@link MavenSourceTree#getRootDirectory()}
         */
        public Set<String> getChildren() {
            return children;
        }

        /**
         * @return a {@link Set} of dependencies declared in this {@link Profile}
         */
        public Set<Dependency> getDependencies() {
            return dependencies;
        }

        /**
         * @return a {@link Set} of dependencyManagement entries declared in this {@link Profile}
         */
        public Set<Dependency> getDependencyManagement() {
            return dependencyManagement;
        }

        public Set<GavExpression> getExtensions() {
            return extensions;
        }

        /**
         * @return an ID of this profile or {@code null} if this is the representation of a top level profile-less
         *         dependencies, plugins, etc.
         */
        public String getId() {
            return id;
        }

        /**
         * @return a {@link Set} of pluginManagement entries declared in this {@link Profile}
         */
        public Set<Plugin> getPluginManagement() {
            return pluginManagement;
        }

        /**
         * @return a {@link Set} of plugins declared in this {@link Profile}
         */
        public Set<Plugin> getPlugins() {
            return plugins;
        }

        /**
         * @return the {@link Map} of properties declared in this {@link Profile}
         */
        public Map<String, Expression> getProperties() {
            return properties;
        }

    }

    private final GavExpression gav;
    private final GavExpression parentGav;
    /** Relative to source tree root directory */
    private final String pomPath;

    private final List<Profile> profiles;

    Module(String pomPath, GavExpression gav, GavExpression parentGa, List<Profile> profiles) {
        super();
        this.pomPath = pomPath;
        this.gav = gav;
        this.parentGav = parentGa;
        this.profiles = profiles;
    }

    /**
     * Goes through active profiles and find the definition of the property having the given {@code propertyName}.
     *
     * @param propertyName the property name to find a definition for
     * @param isProfileActive tells which profiles are active
     * @return the {@link ValueDefinition} of the seeked property or {@code null} if no such property is defined in
     *         this {@link Module}
     */
    public ValueDefinition findPropertyDefinition(String propertyName, Predicate<Profile> isProfileActive) {
        final ListIterator<Profile> it = this.profiles.listIterator(this.profiles.size());
        while (it.hasPrevious()) {
            final Profile p = it.previous();
            if (isProfileActive.test(p)) {
                final Expression result = p.properties.get(propertyName);
                if (result != null) {
                    final String xPath = xPathProfile(p.getId(), "properties", propertyName);
                    return new ValueDefinition(this, xPath, result);
                }
            }
        }
        return null;
    }

    /**
     * @return the {@link GavExpression} of this Maven module
     */
    public GavExpression getGav() {
        return gav;
    }

    /**
     * @return the {@link GavExpression} of the Maven parent module of this module or {@code null} if this module
     *         has no parent
     */
    public GavExpression getParentGav() {
        return parentGav;
    }

    /**
     * @return a path to the this module's {@code pom.xml} relative to {@link MavenSourceTree#getRootModule()}
     */
    public String getPomPath() {
        return pomPath;
    }

    /**
     * @return the {@link List} of profiles defined in this {@link Module}. Note that the top level profile-less
     *         dependencies, dependencyManagement, etc. are defined in {@link Module} with {@code id} {@code null}.
     */
    public List<Profile> getProfiles() {
        return profiles;
    }

    /**
     * @param childPomPath a path to {@code pom.xml} file relative to {@link MavenSourceTree#rootDirectory}
     * @param isProfileActive
     * @return {@code true} if the {@code pom.xml} represented by this {@link Module} has a {@code <module>} with
     *         the given pom.xml path or {@code false} otherwise
     */
    public boolean hasChild(String childPomPath, Predicate<Profile> isProfileActive) {
        for (Profile p : profiles) {
            if (isProfileActive.test(p)) {
                if (p.children.contains(childPomPath)) {
                    return true;
                }
            }
        }
        return false;
    }

} public class Configuration {
    public static class Builder extends DefaultContainerNode<Node> {

        final ScalarNode<SrcVersion> buildRef = new DefaultScalarNode<>("buildRef", SrcVersion.getBranchMaster());
        final ScalarNode<Duration> buildTimeout = new DefaultScalarNode<>("buildTimeout", Duration.maxValue());
        final ScalarNode<Pattern> buildVersionPattern = new DefaultScalarNode<>("buildVersionPattern", null,
                Pattern.class, EqualsImplementations.equalsPattern());
        final ScalarNode<String> configModelVersion = new DefaultScalarNode<>("configModelVersion",
                LATEST_CONFIG_MODEL_VERSION);
        final ScalarNode<Charset> encoding = new DefaultScalarNode<>("encoding", DEFAULT_ENCODING);
        final ScalarNode<Boolean> forwardAsMasterConfig = new DefaultScalarNode<>("forwardAsMasterConfig",
                Boolean.FALSE);
        final ListOfScalarsNode<String> forwardProperties = new DefaultListOfScalarsNode<String>(
                FORWARD_PROPERTIES_ATTRIBUTE, String.class) {

            @Override
            public void applyDefaultsAndInheritance(Stack<Node> configurationStack) {
                if (getElements().isEmpty()) {
                    addAll(DEFAULT_FORWARD_PROPERTIES);
                }
            }

            @Override
            public boolean isInDefaultState(Stack<Node> configurationStack) {
                return getElements().isEmpty() || DEFAULT_FORWARD_PROPERTIES.equals(asSetOfValues());
            }

        };
        private Map<String, String> forwardPropertyValues = new TreeMap<>();
        final ScalarNode<Boolean> logToConsole = new DefaultScalarNode<>("logToConsole", Boolean.TRUE);
        final ScalarNode<Boolean> logToFile = new DefaultScalarNode<>("logToFile", Boolean.TRUE);
        final Maven.Builder maven = Maven.builder();
        final DefaultContainerNode<ScmRepository.Builder> repositories = new DefaultContainerNode<>("repositories");
        final ScalarNode<Boolean> skip = new DefaultScalarNode<>("skip", Boolean.FALSE);
        final ScalarNode<Path> sourcesDirectory = new DefaultScalarNode<>("sourcesDirectory", Path.class);
        final ScalarNode<Verbosity> verbosity = new DefaultScalarNode<>("verbosity", Verbosity.warn);

        private Builder() {
            super("srcdeps");
            addChildren( //
                    configModelVersion, //
                    encoding, //
                    forwardProperties, //
                    forwardAsMasterConfig, //
                    logToFile, //
                    logToConsole, //
                    skip, //
                    sourcesDirectory, //
                    verbosity, //
                    buildTimeout, //
                    buildRef, //
                    buildVersionPattern, //
                    maven, //
                    repositories //
            );
        }

        public Builder accept(Visitor visitor) {
            new TreeWalker().walk(this, visitor);
            return this;
        }

        public Configuration build() {
            Collection<ScmRepository.Builder> repoBuilders = repositories.getChildren().values();
            List<ScmRepository> repos = new ArrayList<>(repoBuilders.size());
            for (ScmRepository.Builder repoBuilder : repoBuilders) {
                repos.add(repoBuilder.build());
            }

            Map<String, String> useFwdPropValues = Collections.unmodifiableMap(forwardPropertyValues);
            this.forwardPropertyValues = null;

            Configuration result = new Configuration( //
                    configModelVersion.getValue(), //
                    forwardAsMasterConfig.getValue(), Collections.unmodifiableList(repos), //
                    sourcesDirectory.getValue(), //
                    skip.getValue(), //
                    forwardProperties.asSetOfValues(), //
                    useFwdPropValues, //
                    maven.build() //
            );
            return result;
        }

        public Builder buildRef(SrcVersion value) {
            this.buildRef.setValue(value);
            return this;
        }

        public Builder buildTimeout(Duration buildTimeout) {
            this.buildTimeout.setValue(buildTimeout);
            return this;
        }

        public Builder buildVersionPattern(Pattern value) {
            this.buildVersionPattern.setValue(value);
            return this;
        }

        public Builder commentBefore(String value) {
            commentBefore.add(value);
            return this;
        }

        public Builder configModelVersion(String configModelVersion) {
            if (!SUPPORTED_CONFIG_MODEL_VERSIONS.contains(configModelVersion)) {
                throw new IllegalArgumentException(
                        String.format("Cannot parse configModelVersion [%s]; expected any of [%s]", configModelVersion,
                                SUPPORTED_CONFIG_MODEL_VERSIONS));
            }
            this.configModelVersion.setValue(configModelVersion);
            return this;
        }

        public Builder encoding(Charset encoding) {
            this.encoding.setValue(encoding);
            return this;
        }

        public Builder forwardAsMasterConfig(boolean value) {
            this.forwardAsMasterConfig.setValue(value);
            return this;
        }

        public Builder forwardProperties(Collection<String> names) {
            forwardProperties.addAll(names);
            return this;
        }

        public Builder forwardProperty(String name) {
            forwardProperties.add(name);
            return this;
        }

        public Builder forwardPropertyValue(String key, String value) {
            forwardPropertyValues.put(key, value);
            return this;
        }

        @Override
        public Map<String, Node> getChildren() {
            return children;
        }

        public Builder logToConsole(boolean value) {
            this.logToConsole.setValue(value);
            return this;
        }

        public Builder logToFile(boolean value) {
            this.logToFile.setValue(value);
            return this;
        }

        public Builder maven(Maven.Builder maven) {
            this.maven.init(maven);
            return this;
        }

        public Builder repositories(Map<String, ScmRepository.Builder> repoBuilders) {
            for (Map.Entry<String, ScmRepository.Builder> en : repoBuilders.entrySet()) {
                ScmRepository.Builder repoBuilder = en.getValue();
                repoBuilder.id(en.getKey());
                this.repositories.addChild(repoBuilder);
            }
            return this;
        }

        public Builder repository(ScmRepository.Builder repo) {
            this.repositories.addChild(repo);
            return this;
        }

        public Builder skip(boolean value) {
            this.skip.setValue(value);
            return this;
        }

        public Builder sourcesDirectory(Path value) {
            this.sourcesDirectory.setValue(value);
            return this;
        }

        public Builder verbosity(Verbosity value) {
            this.verbosity.setValue(value);
            return this;
        }

    }

    private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;

    private static final Set<String> DEFAULT_FORWARD_PROPERTIES;

    private static final String FORWARD_PROPERTIES_ATTRIBUTE = "forwardProperties";
    private static final String LATEST_CONFIG_MODEL_VERSION = "3.0";

    private static final String SRCDEPS_ENCODING_PROPERTY = "srcdeps.encoding";

    private static final String SRCDEPS_MASTER_CONFIG_PROPERTY = "srcdeps.masterConfig";
    private static final Set<String> SUPPORTED_CONFIG_MODEL_VERSIONS = Collections
            .unmodifiableSet(new LinkedHashSet<>(Arrays.asList(LATEST_CONFIG_MODEL_VERSION)));

    static {
        DEFAULT_FORWARD_PROPERTIES = Collections.unmodifiableSet(new LinkedHashSet<>(
                Arrays.asList(SRCDEPS_MASTER_CONFIG_PROPERTY, Maven.getSrcdepsMavenPropertiesPattern())));
    }

    public static Builder builder() {
        return new Builder();
    }

    public static Charset getDefaultEncoding() {
        return DEFAULT_ENCODING;
    }

    /**
     * @return the {@link Set} of system property names to use as {@code forwardProperties}
     */
    public static Set<String> getDefaultForwardProperties() {
        return DEFAULT_FORWARD_PROPERTIES;
    }

    /**
     * @return {@value Configuration#FORWARD_PROPERTIES_ATTRIBUTE} - the name of an attribute found inside
     *         {@code srcdeps.yaml}.
     */
    public static String getForwardPropertiesAttribute() {
        return FORWARD_PROPERTIES_ATTRIBUTE;
    }

    /**
     * @return the latest and default {@code configModelVersion}
     */
    public static String getLatestConfigModelVersion() {
        return LATEST_CONFIG_MODEL_VERSION;
    }

    /**
     * @return the name of the system property that can be used to set an encoding other than the default {@code utf-8}
     *         for reading any srcdeps related content, esp. the {@code srcdeps.yaml} file.
     */
    public static String getSrcdepsEncodingProperty() {
        return SRCDEPS_ENCODING_PROPERTY;
    }

    /**
     * @return {@value Configuration#SRCDEPS_MASTER_CONFIG_PROPERTY} - the name of the system property that can be used
     *         to replace any {@code srcdeps.yaml} configuration present in dependency source trees.
     */
    public static String getSrcdepsMasterConfigProperty() {
        return SRCDEPS_MASTER_CONFIG_PROPERTY;
    }

    /**
     * @return a {@link Set} of supported {@code configModelVersion}s
     */
    public static Set<String> getSupportedConfigModelVersions() {
        return SUPPORTED_CONFIG_MODEL_VERSIONS;
    }

    private final String configModelVersion;
    private final boolean forwardAsMasterConfig;
    private final Set<String> forwardProperties;
    private final transient Map<String, String> forwardPropertyValues;
    private final Maven maven;
    private final List<ScmRepository> repositories;
    private final boolean skip;
    private final Path sourcesDirectory;

    private Configuration(String configModelVersion, boolean forwardAsMasterConfig, List<ScmRepository> repositories,
            Path sourcesDirectory, boolean skip, Set<String> forwardPropertyNames,
            Map<String, String> forwardProperties, Maven maven) {
        super();
        this.configModelVersion = configModelVersion;
        this.forwardAsMasterConfig = forwardAsMasterConfig;
        this.repositories = repositories;
        this.sourcesDirectory = sourcesDirectory;
        this.skip = skip;
        this.forwardProperties = forwardPropertyNames;
        this.forwardPropertyValues = forwardProperties;
        this.maven = maven;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Configuration other = (Configuration) obj;
        if (configModelVersion == null) {
            if (other.configModelVersion != null)
                return false;
        } else if (!configModelVersion.equals(other.configModelVersion))
            return false;
        if (forwardAsMasterConfig != other.forwardAsMasterConfig)
            return false;
        if (forwardProperties == null) {
            if (other.forwardProperties != null)
                return false;
        } else if (!forwardProperties.equals(other.forwardProperties))
            return false;
        if (maven == null) {
            if (other.maven != null)
                return false;
        } else if (!maven.equals(other.maven))
            return false;
        if (repositories == null) {
            if (other.repositories != null)
                return false;
        } else if (!repositories.equals(other.repositories))
            return false;
        if (skip != other.skip)
            return false;
        if (sourcesDirectory == null) {
            if (other.sourcesDirectory != null)
                return false;
        } else if (!sourcesDirectory.equals(other.sourcesDirectory))
            return false;
        return true;
    }

    /**
     * @return the version of the configuration model
     */
    public String getConfigModelVersion() {
        return configModelVersion;
    }

    /**
     * Returns a set of property names that the top level builder A should pass as java system properties to every
     * dependency builder B (using {@code -DmyProperty=myValue}) command line arguments. Further, in case a child
     * builder B spawns its own new child builder C, B must pass all these properties to C in the very same manner as A
     * did to B.
     * <p>
     * A property name may end with asterisk {@code *} to denote that all properties starting with the part before the
     * asterisk should be forwared. E.g. {@code my.prop.*} would forward both {@code my.prop.foo} and
     * {@code my.prop.bar}.
     *
     * @return a {@link Set} of property names
     */
    public Set<String> getForwardProperties() {
        return forwardProperties;
    }

    /**
     * A map of property values that the top level builder A should pass as java system properties to every dependency
     * builder B using {@code -DmyProperty=myValue} style command line arguments. Further, in case a child builder B
     * spawns its own new child builder C, B must pass all these properties to C in the very same manner as A did to B.
     *
     * @return a {@link Map} of properties to forward
     */
    public Map<String, String> getForwardPropertyValues() {
        return forwardPropertyValues;
    }

    /**
     * @return Maven specific settings that apply both on the top level and for each source repository (unless they are
     *         overriden at the repository level).
     */
    public Maven getMaven() {
        return maven;
    }

    /**
     * @return the list of {@link ScmRepository ScmRepositories} that should be used for building of the dependencies
     */
    public List<ScmRepository> getRepositories() {
        return repositories;
    }

    /**
     * Returns a {@link Path} to the sources directory where the dependency sources should be checked out. Each SCM
     * repository will have a subdirectory named after its {@code id} there.
     *
     * @return a {@link Path} to the sources
     */
    public Path getSourcesDirectory() {
        return sourcesDirectory;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((configModelVersion == null) ? 0 : configModelVersion.hashCode());
        result = prime * result + (forwardAsMasterConfig ? 1231 : 1237);
        result = prime * result + ((forwardProperties == null) ? 0 : forwardProperties.hashCode());
        result = prime * result + ((maven == null) ? 0 : maven.hashCode());
        result = prime * result + ((repositories == null) ? 0 : repositories.hashCode());
        result = prime * result + (skip ? 1231 : 1237);
        result = prime * result + ((sourcesDirectory == null) ? 0 : sourcesDirectory.hashCode());
        return result;
    }

    public boolean isForwardAsMasterConfig() {
        return forwardAsMasterConfig;
    }

    /**
     * @return {@code true} if the whole srcdeps processing should be skipped or {@code false} otherwise
     */
    public boolean isSkip() {
        return skip;
    }

    @Override
    public String toString() {
        return "Configuration [configModelVersion=" + configModelVersion + ", forwardAsMasterConfig="
                + forwardAsMasterConfig + ", forwardPropertyNames=" + forwardProperties + ", maven=" + maven
                + ", repositories=" + repositories + ", skip=" + skip + ", sourcesDirectory=" + sourcesDirectory + "]";
    }

} public class Maven {
    public static class Builder extends DefaultContainerNode<Node> {
        final MavenAssertions.FailWithBuilder failWith = MavenAssertions.failWithBuilder();
        final MavenAssertions.FailWithoutBuilder failWithout = MavenAssertions.failWithoutBuilder();
        final ScalarNode<Boolean> useVersionsMavenPlugin = new DefaultScalarNode<>("useVersionsMavenPlugin",
                DEFAULT_USE_VERSIONS_MAVEN_PLUGIN);
        final ScalarNode<String> versionsMavenPluginVersion = new DefaultScalarNode<>("versionsMavenPluginVersion",
                DEFAULT_VERSIONS_MAVEN_PLUGIN_VERSION);

        public Builder() {
            super("maven");
            addChildren(versionsMavenPluginVersion, useVersionsMavenPlugin, failWith, failWithout);
        }

        public Maven build() {
            return new Maven(failWith.build(), failWithout.build());
        }

        public Builder commentBefore(String value) {
            commentBefore.add(value);
            return this;
        }

        public Builder failWith(MavenAssertions.FailWithBuilder failWith) {
            this.failWith.init(failWith);
            return this;
        }

        public Builder failWithout(MavenAssertions.FailWithoutBuilder failWithout) {
            this.failWithout.init(failWithout);
            return this;
        }

        @Override
        public Map<String, Node> getChildren() {
            return children;
        }

        public Builder useVersionsMavenPlugin(boolean useVersionsMavenPlugin) {
            this.useVersionsMavenPlugin.setValue(useVersionsMavenPlugin);
            return this;
        }

        public Builder versionsMavenPluginVersion(String versionsMavenPluginVersion) {
            this.versionsMavenPluginVersion.setValue(versionsMavenPluginVersion);
            return this;
        }

    }

    /** Keep in sync with doc/srcdeps.yaml */
    private static final boolean DEFAULT_USE_VERSIONS_MAVEN_PLUGIN = false;
    private static final String DEFAULT_VERSIONS_MAVEN_PLUGIN_VERSION = "2.3";
    private static final String SRCDEPS_MAVEN_PROPERTIES_PATTERN = "srcdeps.maven.*";
    private static final String SRCDEPS_MAVEN_SETTINGS_PROPERTY = "srcdeps.maven.settings";
    private static final String SRCDEPS_MAVEN_VERSION_PROPERTY = "srcdeps.maven.version";

    public static Builder builder() {
        return new Builder();
    }

    /**
     * @return the default value ({@value #DEFAULT_USE_VERSIONS_MAVEN_PLUGIN}) for
     *         {@link Builder#useVersionsMavenPlugin}
     */
    public static boolean getDefaultUseVersionsMavenPlugin() {
        return DEFAULT_USE_VERSIONS_MAVEN_PLUGIN;
    }

    /**
     * @return the default version of {@code versions-maven-plugin} to use when
     *         {@code srcdeps.maven.versionsMavenPluginVersion} is neither set in {@code srcdeps.yaml} nor on the
     *         command line. The current value is {@value #DEFAULT_VERSIONS_MAVEN_PLUGIN_VERSION}
     */
    public static String getDefaultVersionsMavenPluginVersion() {
        return DEFAULT_VERSIONS_MAVEN_PLUGIN_VERSION;
    }

    /**
     * @return {@value #SRCDEPS_MAVEN_PROPERTIES_PATTERN}
     */
    public static String getSrcdepsMavenPropertiesPattern() {
        return SRCDEPS_MAVEN_PROPERTIES_PATTERN;
    }

    /**
     * @return the name of the system property to pass the path to settings.xml file that should be used in the builds
     *         of source dependencies. The value is {@value #SRCDEPS_MAVEN_SETTINGS_PROPERTY}
     */
    public static String getSrcdepsMavenSettingsProperty() {
        return SRCDEPS_MAVEN_SETTINGS_PROPERTY;
    }

    /**
     * @return the name of the system property to pass the version of {@code srcdeps-maven-plugin} that should be used
     *         in the builds of source dependencies. The value is {@value #SRCDEPS_MAVEN_VERSION_PROPERTY}
     */
    public static String getSrcdepsMavenVersionProperty() {
        return SRCDEPS_MAVEN_VERSION_PROPERTY;
    }

    private final MavenAssertions failWith;

    private final MavenAssertions failWithout;

    private Maven(MavenAssertions failWith, MavenAssertions failWithout) {
        super();
        this.failWith = failWith;
        this.failWithout = failWithout;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Maven other = (Maven) obj;
        if (failWith == null) {
            if (other.failWith != null)
                return false;
        } else if (!failWith.equals(other.failWith))
            return false;
        if (failWithout == null) {
            if (other.failWithout != null)
                return false;
        } else if (!failWithout.equals(other.failWithout))
            return false;
        return true;
    }

    /**
     * To be used to prevent building with srcdeps when any of the returned build arguments is present in the top level
     * build. Note that this list is appended to the default {@code failWithAnyOfArguments} list of the given Build
     * Tool. Maven's default {@code failWithAnyOfArguments} are <code>{"release:prepare", "release:perform"}</code>.
     *
     * @return a {@link Set} of build arguments that make the top level build fail if they are present.
     */
    public MavenAssertions getFailWith() {
        return failWith;
    }

    /**
     * To be used to prevent building with srcdeps when any of the returned build arguments is present in the top level
     * build. Note that this list is appended to the default {@code failWithAnyOfArguments} list of the given Build
     * Tool. Maven's default {@code failWithAnyOfArguments} are <code>{"release:prepare", "release:perform"}</code>.
     *
     * @return a {@link Set} of build arguments that make the top level build fail if they are not present.
     */
    public MavenAssertions getFailWithout() {
        return failWithout;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((failWith == null) ? 0 : failWith.hashCode());
        result = prime * result + ((failWithout == null) ? 0 : failWithout.hashCode());
        return result;
    }

    @Override
    public String toString() {
        return "Maven [failWith=" + failWith + ", failWithout=" + failWithout + "]";
    }
} public class Shell {

    /**
     * A simple wrapper over {@link Process} that manages its destroying and offers Java 8-like
     * {@link #waitFor(long, TimeUnit, String[])} with timeout.
     */
    public static class CommandProcess implements Closeable {

        private final Process process;
        private final Thread shutDownHook;
        private final StreamGobbler stdOut;

        public CommandProcess(String requestId, Process process, LineConsumer out) {
            super();
            this.process = process;
            this.stdOut = new StreamGobbler(process.getInputStream(), out);
            stdOut.start();

            this.shutDownHook = new Thread(new Runnable() {
                @Override
                public void run() {
                    stdOut.cancel();
                    // stdErr.cancel();
                    CommandProcess.this.process.destroy();
                }
            });
            Runtime.getRuntime().addShutdownHook(shutDownHook);
        }

        @Override
        public void close() {
            process.destroy();
        }

        public CommandResult waitFor(long timeout, TimeUnit unit, String[] cmdArray)
                throws CommandTimeoutException, InterruptedException, IOException {
            final long startMillisTime = System.currentTimeMillis();
            final long startNanoTime = System.nanoTime();
            long rem = unit.toNanos(timeout);

            do {
                try {
                    int exitCode = process.exitValue();
                    try {
                        Runtime.getRuntime().removeShutdownHook(shutDownHook);
                    } catch (Exception ignored) {
                    }

                    stdOut.join();
                    stdOut.assertSuccess();

                    return new CommandResult(cmdArray, exitCode, System.currentTimeMillis() - startMillisTime);
                } catch (IllegalThreadStateException ex) {
                    if (rem > 0) {
                        Thread.sleep(Math.min(TimeUnit.NANOSECONDS.toMillis(rem) + 1, 100));
                    }
                }
                rem = unit.toNanos(timeout) - (System.nanoTime() - startNanoTime);
            } while (rem > 0);
            throw new CommandTimeoutException(
                    String.format("Command has not finished within [%d] ms: %s", timeout, Arrays.toString(cmdArray)));
        }

    }

    /**
     * A result of a {@link ShellCommand}'s execution.
     */
    public static class CommandResult {
        private final String[] cmdArray;
        private final int exitCode;
        private final long runtimeMs;

        public CommandResult(String[] cmdArray, int exitCode, long runtimeMs) {
            super();
            this.cmdArray = cmdArray;
            this.exitCode = exitCode;
            this.runtimeMs = runtimeMs;
        }

        /**
         * @return this {@link CommandResult}
         * @throws BadExitCodeException if and only if {@link #exitCode} != 0
         */
        public CommandResult assertSuccess() throws BadExitCodeException {
            if (exitCode != 0) {
                throw new BadExitCodeException(cmdArray, exitCode);
            }
            return this;
        }

        /**
         * @return the exit code returned by the underlying {@link ShellCommand}
         */
        public int getExitCode() {
            return exitCode;
        }

        public long getRuntimeMs() {
            return runtimeMs;
        }
    }

    /**
     * The usual friend of {@link Process#getInputStream()} / {@link Process#getErrorStream()}.
     */
    static class StreamGobbler extends Thread {
        private volatile boolean cancelled;
        private IOException exception;
        private final InputStream in;
        private final LineConsumer out;

        private StreamGobbler(InputStream in, LineConsumer out) {
            this.in = in;
            this.out = out;
        }

        public void assertSuccess() throws IOException {
            if (exception != null) {
                throw exception;
            }
        }

        public void cancel() {
            this.cancelled = true;
        }

        @Override
        public void run() {
            try (BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
                String line;
                while (!cancelled && (line = r.readLine()) != null) {
                    out.accept(line);
                }
            } catch (IOException e) {
                exception = e;
            }
        }
    }

    static final Logger log = LoggerFactory.getLogger(Shell.class);

    /**
     * Executes the given {@link ShellCommand} synchronously.
     *
     * @param command the command to execute
     * @return the {@link CommandResult} that can be used to determine if the execution was successful
     * @throws BuildException on any build related problems
     * @throws CommandTimeoutException if the execution is not finished within the timeout defined in
     *         {@link ShellCommand#getTimeoutMs()}
     */
    public static CommandResult execute(ShellCommand command) throws BuildException, CommandTimeoutException {
        final String[] cmdArray = command.asCmdArray();
        final String cmdArrayString = Arrays.stream(cmdArray).collect(Collectors.joining(" "));
        final Map<String, String> env = command.getEnvironment();
        log.info("srcdeps[{}]: Executing command [{}] using env {}", command.getId(), cmdArrayString, env);
        ProcessBuilder builder = new ProcessBuilder(cmdArray) //
                .directory(command.getWorkingDirectory().toFile()) //
                .redirectErrorStream(true);
        if (!env.isEmpty()) {
            builder.environment().putAll(env);
        }
        try (LineConsumer out = command.getOutput().get();
                CommandProcess process = new CommandProcess(command.getId(), builder.start(), out)) {
            return process.waitFor(command.getTimeoutMs(), TimeUnit.MILLISECONDS, cmdArray).assertSuccess();
        } catch (IOException | InterruptedException e) {
            throw new BuildException(String.format("Could not start command [%s]", cmdArrayString), e);
        }
    }

    /**
     * You are looking for {@link #execute(ShellCommand)}.
     */
    private Shell() {
        super();
    }

} public static class CommandResult {
    private final String[] cmdArray;
    private final int exitCode;
    private final long runtimeMs;

    public CommandResult(String[] cmdArray, int exitCode, long runtimeMs) {
        super();
        this.cmdArray = cmdArray;
        this.exitCode = exitCode;
        this.runtimeMs = runtimeMs;
    }

    /**
     * @return this {@link CommandResult}
     * @throws BadExitCodeException if and only if {@link #exitCode} != 0
     */
    public CommandResult assertSuccess() throws BadExitCodeException {
        if (exitCode != 0) {
            throw new BadExitCodeException(cmdArray, exitCode);
        }
        return this;
    }

    /**
     * @return the exit code returned by the underlying {@link ShellCommand}
     */
    public int getExitCode() {
        return exitCode;
    }

    public long getRuntimeMs() {
        return runtimeMs;
    }
} public class ShellCommand {
    public static class ShellCommandBuilder {
        private List<String> arguments = new ArrayList<>();
        private Map<String, String> environment = new LinkedHashMap<>();
        private String executable;
        private String id;
        private Supplier<LineConsumer> output;
        private long timeoutMs = Long.MAX_VALUE;
        private Path workingDirectory;

        public ShellCommandBuilder arguments(List<String> args) {
            this.arguments.addAll(args);
            return this;
        }

        public ShellCommandBuilder arguments(String... args) {
            for (int i = 0; i < args.length; i++) {
                this.arguments.add(args[i]);
            }
            return this;
        }

        public ShellCommand build() {
            return new ShellCommand(id, executable, Collections.unmodifiableList(arguments), workingDirectory,
                    Collections.unmodifiableMap(environment), output, timeoutMs);
        }

        public ShellCommandBuilder environment(Map<String, String> buildEnvironment) {
            this.environment.putAll(buildEnvironment);
            return this;
        }

        public ShellCommandBuilder environmentEntry(String key, String value) {
            this.environment.put(key, value);
            return this;
        }

        public ShellCommandBuilder executable(String executable) {
            this.executable = executable;
            return this;
        }

        public ShellCommandBuilder id(String id) {
            this.id = id;
            return this;
        }

        public ShellCommandBuilder output(Supplier<LineConsumer> output) {
            this.output = output;
            return this;
        }

        public ShellCommandBuilder timeoutMs(long timeoutMs) {
            this.timeoutMs = timeoutMs;
            return this;
        }

        public ShellCommandBuilder workingDirectory(Path workingDirectory) {
            this.workingDirectory = workingDirectory;
            return this;
        }

    }

    public static ShellCommandBuilder builder() {
        return new ShellCommandBuilder();
    }

    private final List<String> arguments;
    private final Map<String, String> environment;
    private final String executable;
    private final String id;
    private final Supplier<LineConsumer> output;

    private final long timeoutMs;

    private final Path workingDirectory;

    private ShellCommand(String id, String executable, List<String> arguments, Path workingDirectory,
            Map<String, String> environment, Supplier<LineConsumer> output, long timeoutMs) {
        super();
        SrcdepsCoreUtils.assertArgNotNull(id, "id");
        SrcdepsCoreUtils.assertArgNotNull(executable, "executable");
        SrcdepsCoreUtils.assertArgNotNull(arguments, "arguments");
        SrcdepsCoreUtils.assertArgNotNull(workingDirectory, "workingDirectory");
        SrcdepsCoreUtils.assertArgNotNull(environment, "environment");
        SrcdepsCoreUtils.assertArgNotNull(output, "output");

        this.id = id;
        this.executable = executable;
        this.arguments = arguments;
        this.workingDirectory = workingDirectory;
        this.environment = environment;
        this.output = output;
        this.timeoutMs = timeoutMs;
    }

    /**
     * @return an array containing the executable and its arguments that can be passed e.g. to
     *         {@link ProcessBuilder#command(String...)}
     */
    public String[] asCmdArray() {
        String[] result = new String[arguments.size() + 1];
        int i = 0;
        result[i++] = executable;
        for (String arg : arguments) {
            result[i++] = arg;
        }
        return result;
    }

    /**
     * @return the {@link List} arguments for the executable. Cannot be {@code null}.
     */
    public List<String> getArguments() {
        return arguments;
    }

    /**
     * @return a {@link Map} of environment variables that should be used when executing this {@link ShellCommand}.
     *         Cannot be {@code null}. Note that these are just overlay variables - when a new {@link Process} is
     *         spawned, the environment is copied from the present process and only the variables the provided by the
     *         present method are overwritten.
     */
    public Map<String, String> getEnvironment() {
        return environment;
    }

    /**
     * @return the executable file that should be called
     */
    public String getExecutable() {
        return executable;
    }

    public String getId() {
        return id;
    }
    //
    // public Consumer<String> getLogger() {
    // return logger;
    // }

    public Supplier<LineConsumer> getOutput() {
        return output;
    }

    /**
     * @return timeout in milliseconds
     */
    public long getTimeoutMs() {
        return timeoutMs;
    }

    /**
     * @return the directory in which this {@link ShellCommand} should be executed
     */
    public Path getWorkingDirectory() {
        return workingDirectory;
    }

}
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.srcdeps.core.BuildException;
import org.srcdeps.core.BuildRequest;
import org.srcdeps.core.BuildRequest.Verbosity;
import org.srcdeps.core.Ga;
import org.srcdeps.core.MavenSourceTree;
import org.srcdeps.core.MavenSourceTree.ActiveProfiles;
import org.srcdeps.core.MavenSourceTree.Module;
import org.srcdeps.core.config.Configuration;
import org.srcdeps.core.config.Maven;
import org.srcdeps.core.shell.Shell;
import org.srcdeps.core.shell.Shell.CommandResult;
import org.srcdeps.core.shell.ShellCommand;
/**
 * Copyright 2015-2019 Maven Source Dependencies
 * Plugin contributors as indicated by the @author tags.
 *
 * Licensed 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
 *
 *   http://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.srcdeps.core.impl.builder;



/**
 * A base for {@link MvnBuilder} and {@link MvnwBuilder}.
 *
 * @author <a href="https://github.com/ppalaga">Peter Palaga</a>
 */
public abstract class AbstractMvnBuilder extends ShellBuilder {
    private static final Logger log = LoggerFactory.getLogger(AbstractMvnBuilder.class);
    protected static final List<String> MVN_DEFAULT_ARGS = Collections
            .unmodifiableList(Arrays.asList("clean", "install"));
    protected static final Map<String, String> MVN_DEFAULT_BUILD_ENVIRONMENT = Collections.emptyMap();

    protected static final List<String> MVNW_FILE_NAMES = Collections
            .unmodifiableList(Arrays.asList("mvnw", "mvnw.cmd"));
    protected static final List<String> POM_FILE_NAMES = Collections.unmodifiableList(
            Arrays.asList("pom.xml", "pom.atom", "pom.clj", "pom.groovy", "pom.rb", "pom.scala", "pom.yml"));
    protected static final List<String> SKIP_TESTS_ARGS = Collections.singletonList("-DskipTests");

    /**
     * @return the default build arguments used in Maven builds of source dependencies
     */
    public static List<String> getMvnDefaultArgs() {
        return MVN_DEFAULT_ARGS;
    }

    /**
     * @return the list of file names whose presence signals that the project can be built with {@link MvnwBuilder}
     */
    public static List<String> getMvnwFileNames() {
        return MVNW_FILE_NAMES;
    }

    /**
     * @return the list of file names that can store Maven Project Object Model. NB: there is not only {@code pom.xml}
     */
    public static List<String> getPomFileNames() {
        return POM_FILE_NAMES;
    }

    /**
     * @return the {@link List} of arguments to use when no tests should be run during the build of a source dependency
     */
    public static List<String> getSkipTestsArgs() {
        return SKIP_TESTS_ARGS;
    }

    public static boolean hasMvnwFile(Path directory) {
        for (String fileName : MVNW_FILE_NAMES) {
            if (directory.resolve(fileName).toFile().exists()) {
                return true;
            }
        }
        return false;
    }

    public static boolean hasPomFile(Path directory) {
        for (String fileName : POM_FILE_NAMES) {
            if (directory.resolve(fileName).toFile().exists()) {
                return true;
            }
        }
        return false;
    }

    public AbstractMvnBuilder(String executable) {
        super(executable);
    }

    private void addBuildIncludes(BuildRequest request, final List<String> args) throws BuildException {