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());
    }

} 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 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();
} 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;
    }

} 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 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;
    }

} 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);
    }
} public class BadExitCodeException extends BuildException {
    private static final long serialVersionUID = 5437686562879042682L;
    private final int exitCode;

    public BadExitCodeException(String[] cmdArray, int exitCode) {
        super(String.format("Command returned exit code [%d]: %s", exitCode, Arrays.toString(cmdArray)));
        this.exitCode = exitCode;
    }

    /**
     * @return the exist code of a command that caused this {@link BadExitCodeException} to be thrown
     */
    public int getExitCode() {
        return exitCode;
    }

} public class CommandTimeoutException extends BuildException {

    private static final long serialVersionUID = -8311365402356710884L;

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

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

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

} public interface LineConsumer extends Consumer<String>, Closeable {

    class LoggerLineConsumer implements LineConsumer {

        private final String id;
        private Logger logger;

        LoggerLineConsumer(String id, Logger logger) {
            super();
            this.id = id;
            this.logger = logger;
        }

        @Override
        public void accept(String t) {
            logger.info("srcdeps[{}]: {}", id, t);
        }

        @Override
        public void close() throws IOException {
        }
    }

    class StringLineConsumer implements LineConsumer {

        private final StringBuilder sb = new StringBuilder();

        @Override
        public void accept(String t) {
            sb.append(t).append('\n');
        }

        @Override
        public void close() throws IOException {
        }

        @Override
        public String toString() {
            return sb.toString();
        }

    }

    class TeeLineConsumer implements LineConsumer {

        private final LineConsumer delegate1;
        private final LineConsumer delegate2;

        TeeLineConsumer(LineConsumer delegate1, LineConsumer delegate2) {
            super();
            this.delegate1 = delegate1;
            this.delegate2 = delegate2;
        }

        @Override
        public void accept(String t) {
            delegate1.accept(t);
            delegate2.accept(t);
        }

        @Override
        public void close() throws IOException {
            delegate1.close();
            delegate2.close();
        }

    }

    class WriterLineConsumer implements LineConsumer {

        private final Writer delegate;
        private final String id;

        WriterLineConsumer(String id, Writer delegate) {
            super();
            this.id = id;
            this.delegate = delegate;
        }

        @Override
        public void accept(String t) {
            try {
                delegate.write(t);
                delegate.write('\n');
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public void close() throws IOException {
            delegate.close();
        }

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

    }

    LineConsumer DUMMY = new LineConsumer() {

        @Override
        public void accept(String t) {
        }

        @Override
        public void close() throws IOException {
        }

        @Override
        public String toString() {
            return "DummyLineConsumer";
        }

    };

    static LineConsumer dummy() {
        return DUMMY;
    }

    static LineConsumer logger(String id, Logger logger) {
        return new LoggerLineConsumer(id, logger);
    }

    static void renameIfNeeded(Path path, int level, int count, String base, String ext) throws IOException {
        final int newLevel = level + 1;
        if (newLevel < count) {
            final Path newPath = path.getParent().resolve(base + "-" + newLevel + ext);
            renameIfNeeded(newPath, newLevel, count, base, ext);
            if (Files.exists(path)) {
                Files.move(path, newPath, StandardCopyOption.REPLACE_EXISTING);
            }
        }
    }

    static LineConsumer rotate(Path path, int count) {
        final String fileName = path.getFileName().toString();
        final int periodPos = fileName.lastIndexOf('.');
        final String base = periodPos >= 0 ? fileName.substring(0, periodPos) : fileName;
        final String ext = periodPos >= 0 ? fileName.substring(periodPos) : "";
        try {
            renameIfNeeded(path, 0, count, base, ext);
            return new WriterLineConsumer(path.toString(), Files.newBufferedWriter(path, StandardCharsets.UTF_8));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    static LineConsumer string() {
        return new StringLineConsumer();
    }

    static LineConsumer tee(LineConsumer lc1, LineConsumer lc2) {
        return new TeeLineConsumer(lc1, lc2);
    }

} 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 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;
    }

} 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 class SrcdepsCoreUtils {

    /** The number of attempts to try when creating a new directory */
    private static final int CREATE_RETRY_COUNT = 256;
    private static final long DELETE_RETRY_MILLIS = 5000L;

    private static final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");

    public static void assertArgNotEmptyString(String value, String argName) {
        if (value.isEmpty()) {
            throw new IllegalArgumentException(String.format("Argument [%s] cannot be an empty String", argName));
        }
    }

    public static void assertArgNotNull(Object value, String argName) {
        if (value == null) {
            throw new IllegalArgumentException(String.format("Argument [%s] cannot be null", argName));
        }
    }

    public static void assertCollectionNotEmpty(Collection<?> value, String argName) {
        if (value.isEmpty()) {
            throw new IllegalArgumentException(String.format("Argument [%s] cannot be an empty collection", argName));
        }
    }

    /**
     * @param bytes the bytes to format
     * @return the given {@code bytes} formatted as a hex string
     */
    public static String bytesToHexString(byte[] bytes) {
        return bytesToHexString(bytes, 0, bytes.length);
    }

    /**
     * @param bytes the bytes to format
     * @param offset the offset to start at
     * @param length how many bytes from offset to format
     * @return the given {@code bytes} formatted as a hex string
     */
    public static String bytesToHexString(byte[] bytes, int offset, int length) {
        StringBuilder sb = new StringBuilder(length * 2);
        for (int i = offset; i < offset + length; i++) {
            byte b = bytes[i];
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    /**
     * Copy the given {@code src} directory to the given {@code destination} directory.
     *
     * @param src the directory to copy
     * @param destination where to copy
     * @throws IOException
     */
    public static void copyDirectory(final Path src, final Path destination) throws IOException {
        Files.walkFileTree(src, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs)
                    throws IOException {
                Files.createDirectories(destination.resolve(src.relativize(dir)));
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
                Files.copy(file, destination.resolve(src.relativize(file)));
                return FileVisitResult.CONTINUE;
            }
        });
    }

    /**
     * Deletes a file or directory recursively if it exists.
     *
     * @param directory the directory to delete
     * @throws IOException
     */
    public static void deleteDirectory(Path directory) throws IOException {
        if (Files.exists(directory)) {
            Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    if (exc == null) {
                        Files.delete(dir);
                        return FileVisitResult.CONTINUE;
                    } else {
                        // directory iteration failed; propagate exception
                        throw exc;
                    }
                }

                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    if (isWindows) {
                        final long deadline = System.currentTimeMillis() + DELETE_RETRY_MILLIS;
                        FileSystemException lastException = null;
                        do {
                            try {
                                Files.delete(file);
                                return FileVisitResult.CONTINUE;
                            } catch (FileSystemException e) {
                                lastException = e;
                            }
                        } while (System.currentTimeMillis() < deadline);
                        throw new IOException(String.format("Could not delete file [%s] after retrying for %d ms", file,
                                DELETE_RETRY_MILLIS), lastException);
                    } else {
                        Files.delete(file);
                    }
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                    // try to delete the file anyway, even if its attributes
                    // could not be read, since delete-only access is
                    // theoretically possible
                    Files.delete(file);
                    return FileVisitResult.CONTINUE;
                }
            });
        }
    }

    /**
     * Makes sure that the given directory exists. Tries creating {@link #CREATE_RETRY_COUNT} times.
     *
     * @param dir the directory {@link Path} to check
     * @throws IOException if the directory could not be created or accessed
     */
    public static void ensureDirectoryExists(Path dir) throws IOException {
        Throwable toThrow = null;
        for (int i = 0; i < CREATE_RETRY_COUNT; i++) {
            try {
                Files.createDirectories(dir);
                if (Files.exists(dir)) {
                    return;
                }
            } catch (AccessDeniedException e) {
                toThrow = e;
                /* Workaround for https://bugs.openjdk.java.net/browse/JDK-8029608 */
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e1) {
                    Thread.currentThread().interrupt();
                    toThrow = e1;
                }
            } catch (IOException e) {
                toThrow = e;
            }
        }
        if (toThrow != null) {
            throw new IOException(String.format("Could not create directory [%s]", dir), toThrow);
        } else {
            throw new IOException(
                    String.format("Could not create directory [%s] attempting [%d] times", dir, CREATE_RETRY_COUNT));
        }

    }

    /**
     * If the given directory does not exist, creates it using {@link #ensureDirectoryExists(Path)}. Otherwise
     * recursively deletes all subpaths in the given directory.
     *
     * @param dir the directory to check
     * @throws IOException if the directory could not be created, accessed or its children deleted
     */
    public static void ensureDirectoryExistsAndEmpty(Path dir) throws IOException {
        if (Files.exists(dir)) {
            try (DirectoryStream<Path> subPaths = Files.newDirectoryStream(dir)) {
                for (Path subPath : subPaths) {
                    if (Files.isDirectory(subPath)) {
                        deleteDirectory(subPath);
                    } else {
                        Files.delete(subPath);
                    }
                }
            }
        } else {
            ensureDirectoryExists(dir);
        }
    }

    /**
     * Climbs up the file system hierarchy starting in {@code start} and searching for a {@code mvnw} or
     * {@code mvnw.cmd} file suitable for the current platform.
     *
     * @param start a preferably absolute path to a file or directory where the search should start
     * @return a path to {@code mvnw} or {@code mvnw.cmd} (suitable for the current platform)
     * @throws IllegalStateException if no {@code mvnw} or {@code mvnw.cmd} could be found
     */
    public static Path findMvnw(final Path start) {
        Path dir = Files.isDirectory(start) ? start : start.getParent();
        final Path mvnwRelPath = Paths.get("mvnw" + (isWindows ? ".cmd" : ""));
        while (dir != null) {
            final Path result = dir.resolve(mvnwRelPath);
            if (Files.exists(result)) {
                return result;
            }
            dir = dir.getParent();
        }
        throw new IllegalStateException(String.format("Could not find mvnw starting in [%s]", start));
    }

    /**
     * @return the file system path to the Java binary that runs the current Java process
     */
    public static String getCurrentJavaExecutable() {
        if (isWindows) {
            return System.getProperties().getProperty("java.home") + File.separator + "bin" + File.separator
                    + "java.exe";
        } else {
            return System.getProperties().getProperty("java.home") + File.separator + "bin" + File.separator + "java";
        }
    }

    /**
     * @return {@code true} if the operating system the present JVM runs on is Windows; {@code false} otherwise
     */
    public static boolean isWindows() {
        return isWindows;
    }

    /**
     * For manual testing of {@link #sha1HexString(Path)} of files larger than the limit for reading the file all at
     * once.
     */
    public static void main(String[] args) throws NoSuchAlgorithmException, IOException {
        System.out.println(sha1HexString(Paths.get(args[0])));
    }

    /**
     * Returns the content of the given {@code reader} as string.
     *
     * @param reader the {@link Reader} to read from
     * @param buf a properly dimensioned buffer to use when reading
     * @return the content read from the given {@code reader}
     * @throws IOException
     */
    public static String read(Reader reader, char[] buf) throws IOException {
        StringBuilder result = new StringBuilder();
        int n;
        while ((n = reader.read(buf)) >= 0) {
            result.append(buf, 0, n);
        }
        return result.toString();
    }

    /**
     * Opens am {@link InputStream} out of the given {@code url} and returns the content as a UTF-8 string.
     *
     * @param url the {@link URL} to read from
     * @param buf a properly dimensioned buffer to use when reading
     * @return the content read from the given {@code url}
     * @throws IOException
     */
    public static String read(URL url, char[] buf) throws IOException {
        try (Reader in = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)) {
            return read(in, buf);
        }
    }

    /**
     * @param artifactPath the {@link Path} of the file whose sha1 should be computed
     * @return the sha1 of the file formatted as a hex string
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public static String sha1HexString(Path artifactPath) throws IOException, NoSuchAlgorithmException {
        if (Files.exists(artifactPath)) {
            final MessageDigest sha1Digest = MessageDigest.getInstance("SHA-1");
            final int bufferSize = 4096;
            final byte[] localMvnRepoArtifactSha1Bytes;
            if (Files.size(artifactPath) <= bufferSize) {
                localMvnRepoArtifactSha1Bytes = sha1Digest.digest(Files.readAllBytes(artifactPath));
            } else {
                try (InputStream in = Files.newInputStream(artifactPath);
                        DigestOutputStream out = new DigestOutputStream(sha1Digest)) {
                    byte[] buffer = new byte[bufferSize];
                    int len;
                    while ((len = in.read(buffer)) >= 0) {
                        out.write(buffer, 0, len);
                    }
                }
                localMvnRepoArtifactSha1Bytes = sha1Digest.digest();
            }
            return SrcdepsCoreUtils.bytesToHexString(localMvnRepoArtifactSha1Bytes);
        } else {
            return null;
        }
    }

    /**
     * @param anyPath a path with either slashes or backslashes
     * @return a file path with slashes
     */
    public static String toUnixPath(String anyPath) {
        if (anyPath == null || anyPath.isEmpty()) {
            return anyPath;
        }
        return anyPath.replace('\\', '/');
    }

    private SrcdepsCoreUtils() {
    }

}
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.junit.Assert;
import org.junit.Test;
import org.srcdeps.core.MavenSourceTree.ActiveProfiles;
import org.srcdeps.core.MavenSourceTree.Builder;
import org.srcdeps.core.MavenSourceTree.Expression;
import org.srcdeps.core.MavenSourceTree.GavExpression;
import org.srcdeps.core.MavenSourceTree.Module;
import org.srcdeps.core.MavenSourceTree.Module.Profile;
import org.srcdeps.core.MavenSourceTree.Module.Profile.PropertyBuilder;
import org.srcdeps.core.shell.BadExitCodeException;
import org.srcdeps.core.shell.CommandTimeoutException;
import org.srcdeps.core.shell.LineConsumer;
import org.srcdeps.core.shell.Shell;
import org.srcdeps.core.shell.ShellCommand;
import org.srcdeps.core.shell.ShellCommand.ShellCommandBuilder;
import org.srcdeps.core.util.SrcdepsCoreUtils;

        Assert.assertEquals(12, b.modulesByGa.size());
        Assert.assertEquals(12, b.modulesByPath.size());

        final Module.Builder parent = b.modulesByGa.get(Ga.of("org.srcdeps.tree-1:tree-parent"));
        Assert.assertTrue(b.modulesByPath.get("pom.xml") == parent);
        Assert.assertEquals("pom.xml", parent.pomPath);
        final GavExpression treeParentGav = moduleGae("org.srcdeps.tree-1:tree-parent:0.0.1");
        Assert.assertEquals(treeParentGav, parent.moduleGav.build());
        Assert.assertEquals(moduleGae("org.srcdeps.external:external-parent:1.2.3"), parent.parentGav.build());

        Assert.assertEquals(new LinkedHashSet<String>(Arrays.asList("module-1/pom.xml", "module-2/pom.xml",
                "module-3/pom.xml", "module-4/pom.xml", "module-6/pom.xml", "module-7/pom.xml", "plugin/pom.xml",
                "proper-parent/pom.xml", "declared-parent/pom.xml")), parent.profiles.get(0).children);
        Assert.assertEquals(Collections.emptyList(), parent.profiles.get(0).dependencies.stream()
                .map(bu -> bu.build().toString()).collect(Collectors.toList()));
        Assert.assertEquals(//
                props(treeParentGav.resolveGa(null, null), "prop1", "val-parent").entrySet(), //
                parent.profiles.get(0).properties.stream().map(PropertyBuilder::build)
                        .collect(Collectors.toCollection(LinkedHashSet::new)));

        {
            final Module.Builder properParent = b.modulesByGa.get(Ga.of("org.srcdeps.tree-1:proper-parent"));
            Assert.assertTrue(b.modulesByPath.get("proper-parent/pom.xml") == properParent);
            Assert.assertEquals("proper-parent/pom.xml", properParent.pomPath);
            GavExpression gav = moduleGae("org.srcdeps.tree-1:proper-parent:0.0.1");
            Assert.assertEquals(gav, properParent.moduleGav.build());
            Assert.assertEquals(treeParentGav, properParent.parentGav.build());
            Assert.assertEquals(new LinkedHashSet<String>(Arrays.asList("proper-parent/module-5/pom.xml")),
                    properParent.profiles.get(0).children);
            Assert.assertEquals(Collections.emptyList(), properParent.profiles.get(0).dependencies.stream()
                    .map(bu -> bu.build().toString()).collect(Collectors.toList()));
            Assert.assertEquals(//
                    props(gav.resolveGa(null, null), "prop1", "val-proper-parent").entrySet(), //
                    properParent.profiles.get(0).properties.stream().map(PropertyBuilder::build)
                            .collect(Collectors.toCollection(LinkedHashSet::new)));
        }

        {
            final Module.Builder m1 = b.modulesByGa.get(Ga.of("org.srcdeps.tree-1:tree-module-1"));
            Assert.assertTrue(b.modulesByPath.get("module-1/pom.xml") == m1);
            Assert.assertEquals("module-1/pom.xml", m1.pomPath);
            Assert.assertEquals(moduleGae("org.srcdeps.tree-1:tree-module-1:0.0.1"), m1.moduleGav.build());
            Assert.assertEquals(treeParentGav, m1.parentGav.build());
            Assert.assertEquals(Arrays.asList("org.srcdeps.external:artifact-3:1.2.3"), m1.profiles.get(0).dependencies
                    .stream().map(bu -> bu.build().toString()).collect(Collectors.toList()));
            Assert.assertEquals(Collections.emptySet(), m1.profiles.get(0).children);
        }

        {
            final Module.Builder m2 = b.modulesByGa.get(Ga.of("org.srcdeps.tree-1:tree-module-2"));
            Assert.assertTrue(b.modulesByPath.get("module-2/pom.xml") == m2);
            Assert.assertEquals("module-2/pom.xml", m2.pomPath);
            Assert.assertEquals(moduleGae("org.srcdeps.tree-1:tree-module-2:0.0.1"), m2.moduleGav.build());
            Assert.assertEquals(treeParentGav, m2.parentGav.build());
            Assert.assertEquals(
                    Arrays.asList("org.srcdeps.tree-1:tree-module-4:0.0.1", "org.srcdeps.tree-1:tree-module-7:0.0.1",
                            "org.srcdeps.tree-1:tree-module-8:0.0.1", "org.srcdeps.external:artifact-4:1.2.3"),
                    m2.profiles.get(0).dependencies.stream().map(bu -> bu.build().toString())
                            .collect(Collectors.toList()));
            Assert.assertEquals(Collections.emptySet(), m2.profiles.get(0).children);
        }

        {
            final Module.Builder m3 = b.modulesByGa.get(Ga.of("org.srcdeps.tree-1:tree-module-3"));
            Assert.assertTrue(b.modulesByPath.get("module-3/pom.xml") == m3);
            Assert.assertEquals("module-3/pom.xml", m3.pomPath);
            Assert.assertEquals(moduleGae("org.srcdeps.tree-1:tree-module-3:0.0.1"), m3.moduleGav.build());
            Assert.assertEquals(treeParentGav, m3.parentGav.build());
            Assert.assertEquals(Arrays.asList("org.srcdeps.external:artifact-1:1.2.3"), m3.profiles.get(0).dependencies
                    .stream().map(bu -> bu.build().toString()).collect(Collectors.toList()));
            Assert.assertEquals(Collections.emptySet(), m3.profiles.get(0).children);
        }

        {
            final Module.Builder m4 = b.modulesByGa.get(Ga.of("org.srcdeps.tree-1:tree-module-4"));
            Assert.assertTrue(b.modulesByPath.get("module-4/pom.xml") == m4);
            Assert.assertEquals("module-4/pom.xml", m4.pomPath);
            Assert.assertEquals(moduleGae("org.srcdeps.tree-1:tree-module-4:0.0.1"), m4.moduleGav.build());
            Assert.assertEquals(treeParentGav, m4.parentGav.build());
            Assert.assertEquals(
                    Arrays.asList("org.srcdeps.tree-1:tree-module-1:0.0.1", "org.srcdeps.tree-1:tree-module-5:0.0.1"),
                    m4.profiles.get(0).dependencies.stream().map(bu -> bu.build().toString())
                            .collect(Collectors.toList()));
            Assert.assertEquals(Collections.emptySet(), m4.profiles.get(0).children);
        }

        {
            final Module.Builder m5 = b.modulesByGa.get(Ga.of("org.srcdeps.tree-1:tree-module-5"));
            Assert.assertTrue(b.modulesByPath.get("proper-parent/module-5/pom.xml") == m5);
            Assert.assertEquals("proper-parent/module-5/pom.xml", m5.pomPath);
            GavExpression gav = moduleGae("org.srcdeps.tree-1:tree-module-5:0.0.1");
            Assert.assertEquals(gav, m5.moduleGav.build());
            Assert.assertEquals(treeParentGav, m5.parentGav.build());
            Assert.assertEquals(Collections.emptyList(), m5.profiles.get(0).dependencies.stream()
                    .map(bu -> bu.build().toString()).collect(Collectors.toList()));
            Assert.assertEquals(Collections.emptySet(), m5.profiles.get(0).children);
            Assert.assertEquals(//
                    props(gav.resolveGa(null, null), "prop1", "val-5").entrySet(), //
                    m5.profiles.get(0).properties.stream().map(PropertyBuilder::build)
                            .collect(Collectors.toCollection(LinkedHashSet::new)));
        }

        final MavenSourceTree t = b.build();
        Assert.assertEquals(new Expression.Constant("val-parent"),
                t.getRootModule().findPropertyDefinition("prop1", ActiveProfiles.of()).getValue());
        {
            final Module m8 = t.getModulesByGa().get(Ga.of("org.srcdeps.tree-1:tree-module-8"));
            Assert.assertEquals(new Expression.Constant("val-8/main"),
                    m8.findPropertyDefinition("prop2", ActiveProfiles.of()).getValue());
            Assert.assertEquals(new Expression.Constant("val-8/p1"),
                    m8.findPropertyDefinition("prop2", ActiveProfiles.of("p1")).getValue());
            Assert.assertEquals(new Expression.Constant("val-8/p2"),
                    m8.findPropertyDefinition("prop2", ActiveProfiles.of("p2")).getValue());
            Assert.assertEquals(new Expression.Constant("val-8/p2"),
                    m8.findPropertyDefinition("prop2", ActiveProfiles.of("p1", "p2")).getValue());
            Assert.assertEquals(new Expression.Constant("val-8/p2"),
                    m8.findPropertyDefinition("prop2", ActiveProfiles.of("p1", "p2")).getValue());
        }

        final Predicate<Profile> profileSelector = p -> true;