Repository: mojohaus/versions
Pull request #1221 (2025-03-08 07:45:58)

Pom helper refactoring
Second stage of refactoring, this time mainly focused on PomHelper


Patch:
diff --git a/pom.xml b/pom.xml
index 972843b1..33688c80 100644
--- a/pom.xml
+++ b/pom.xml
@@ -137,7 +137,7 @@
     <fluidoVersion>${maven-fluido-skin.version}</fluidoVersion>
     <modelloNamespaceRuleVersion>2.1.0</modelloNamespaceRuleVersion>
     <modelloNamespaceReportVersion>2.0.0</modelloNamespaceReportVersion>
-    <byteBuddyVersion>1.17.1</byteBuddyVersion>
+    <byteBuddyVersion>1.17.2</byteBuddyVersion>
 
     <!-- use the same version as in Maven core -->
     <mavenResolverVersion>1.4.1</mavenResolverVersion>
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/api/DefaultVersionsHelper.java b/versions-common/src/main/java/org/codehaus/mojo/versions/api/DefaultVersionsHelper.java
index 1024fbcf..48d0f0ac 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/api/DefaultVersionsHelper.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/api/DefaultVersionsHelper.java
@@ -159,20 +159,24 @@ public class DefaultVersionsHelper implements VersionsHelper {
 
     private final List<RemoteRepository> remoteRepositories;
 
+    private final PomHelper pomHelper;
+
     /**
      * Private constructor used by the builder
      */
     private DefaultVersionsHelper(
+            PomHelper pomHelper,
             ArtifactFactory artifactFactory,
             RepositorySystem repositorySystem,
             MavenSession mavenSession,
             MojoExecution mojoExecution,
             Log log) {
+        this.pomHelper = requireNonNull(pomHelper);
         this.artifactFactory = requireNonNull(artifactFactory);
         this.repositorySystem = repositorySystem;
         this.mavenSession = mavenSession;
         this.mojoExecution = mojoExecution;
-        this.log = log;
+        this.log = requireNonNull(log);
 
         this.remoteProjectRepositories = Optional.ofNullable(mavenSession)
                 .map(MavenSession::getCurrentProject)
@@ -243,11 +247,6 @@ public class DefaultVersionsHelper implements VersionsHelper {
         return (uri != null && uri.startsWith(CLASSPATH_PROTOCOL + ":"));
     }
 
-    @Override
-    public Log getLog() {
-        return log;
-    }
-
     @Override
     public ArtifactVersions lookupArtifactVersions(
             Artifact artifact, VersionRange versionRange, boolean usePluginRepositories)
@@ -261,8 +260,8 @@ public class DefaultVersionsHelper implements VersionsHelper {
             throws VersionRetrievalException {
         try {
             Collection<IgnoreVersion> ignoredVersions = getIgnoredVersions(artifact);
-            if (!ignoredVersions.isEmpty() && getLog().isDebugEnabled()) {
-                getLog().debug("Found ignored versions: " + ignoredVersions + " for artifact" + artifact);
+            if (!ignoredVersions.isEmpty() && log.isDebugEnabled()) {
+                log.debug("Found ignored versions: " + ignoredVersions + " for artifact" + artifact);
             }
 
             final List<RemoteRepository> repositories;
@@ -296,8 +295,8 @@ public class DefaultVersionsHelper implements VersionsHelper {
                             .stream()
                             .filter(v -> ignoredVersions.stream().noneMatch(i -> {
                                 if (IgnoreVersionHelper.isVersionIgnored(v, i)) {
-                                    if (getLog().isDebugEnabled()) {
-                                        getLog().debug("Version " + v + " for artifact "
+                                    if (log.isDebugEnabled()) {
+                                        log.debug("Version " + v + " for artifact "
                                                 + ArtifactUtils.versionlessKey(artifact)
                                                 + " found on ignore list: "
                                                 + i);
@@ -334,7 +333,7 @@ public class DefaultVersionsHelper implements VersionsHelper {
             if (IgnoreVersionHelper.isValidType(ignoreVersion)) {
                 ret.add(ignoreVersion);
             } else {
-                getLog().warn("The type attribute '" + ignoreVersion.getType() + "' for global ignoreVersion["
+                log.warn("The type attribute '" + ignoreVersion.getType() + "' for global ignoreVersion["
                         + ignoreVersion + "] is not valid. Please use one of '" + IgnoreVersionHelper.VALID_TYPES
                         + "'.");
             }
@@ -347,7 +346,7 @@ public class DefaultVersionsHelper implements VersionsHelper {
                 if (IgnoreVersionHelper.isValidType(ignoreVersion)) {
                     ret.add(ignoreVersion);
                 } else {
-                    getLog().warn("The type attribute '" + ignoreVersion.getType() + "' for " + rule + " is not valid."
+                    log.warn("The type attribute '" + ignoreVersion.getType() + "' for " + rule + " is not valid."
                             + " Please use one of '" + IgnoreVersionHelper.VALID_TYPES + "'.");
                 }
             }
@@ -561,25 +560,26 @@ public class DefaultVersionsHelper implements VersionsHelper {
 
         // Auto-link items if required
         if (request.isAutoLinkItems()) {
-            final PropertyVersionsBuilder[] propertyVersionsBuilders;
             try {
-                propertyVersionsBuilders = PomHelper.getPropertyVersionsBuilders(
-                        this, artifactFactory, request.getMavenProject(), request.isIncludeParent());
+                PropertyVersionsBuilder[] propertyVersionsBuilders = pomHelper.getPropertyVersionsBuilders(
+                        this, log, request.getMavenProject(), request.isIncludeParent());
+
+                for (PropertyVersionsBuilder builder : propertyVersionsBuilders) {
+                    String propertyName = builder.getName();
+                    builders.put(propertyName, builder);
+
+                    properties.computeIfAbsent(propertyName, name -> {
+                        Property property = new Property(name);
+                        log.debug(String.format(
+                                "Property ${%s}: Adding inferred version range of %s",
+                                name, builder.getVersionRange()));
+                        property.setVersion(builder.getVersionRange());
+                        return property;
+                    });
+                }
             } catch (ExpressionEvaluationException | IOException e) {
                 throw new MojoExecutionException(e.getMessage(), e);
             }
-
-            for (PropertyVersionsBuilder propertyVersionsBuilder : propertyVersionsBuilders) {
-                final String propertyName = propertyVersionsBuilder.getName();
-                builders.put(propertyName, propertyVersionsBuilder);
-                if (!properties.containsKey(propertyName)) {
-                    final Property property = new Property(propertyName);
-                    getLog().debug("Property ${" + propertyName + "}: Adding inferred version range of "
-                            + propertyVersionsBuilder.getVersionRange());
-                    property.setVersion(propertyVersionsBuilder.getVersionRange());
-                    properties.put(propertyName, property);
-                }
-            }
         }
 
         List<String> includePropertiesList = request.getIncludeProperties() != null
@@ -589,18 +589,18 @@ public class DefaultVersionsHelper implements VersionsHelper {
                 ? Arrays.asList(request.getExcludeProperties().split("\\s*,\\s*"))
                 : Collections.emptyList();
 
-        getLog().debug("Searching for properties associated with builders");
+        log.debug("Searching for properties associated with builders");
         Iterator<Property> i = properties.values().iterator();
         while (i.hasNext()) {
             Property property = i.next();
 
-            getLog().debug("includePropertiesList:" + includePropertiesList + " property: " + property.getName());
-            getLog().debug("excludePropertiesList:" + excludePropertiesList + " property: " + property.getName());
+            log.debug("includePropertiesList:" + includePropertiesList + " property: " + property.getName());
+            log.debug("excludePropertiesList:" + excludePropertiesList + " property: " + property.getName());
             if (!includePropertiesList.isEmpty() && !includePropertiesList.contains(property.getName())) {
-                getLog().debug("Skipping property ${" + property.getName() + "}");
+                log.debug("Skipping property ${" + property.getName() + "}");
                 i.remove();
             } else if (!excludePropertiesList.isEmpty() && excludePropertiesList.contains(property.getName())) {
-                getLog().debug("Ignoring property ${" + property.getName() + "}");
+                log.debug("Ignoring property ${" + property.getName() + "}");
                 i.remove();
             }
         }
@@ -608,21 +608,21 @@ public class DefaultVersionsHelper implements VersionsHelper {
         Map<Property, PropertyVersions> propertyVersions = new LinkedHashMap<>(properties.size());
         while (i.hasNext()) {
             Property property = i.next();
-            getLog().debug("Property ${" + property.getName() + "}");
+            log.debug("Property ${" + property.getName() + "}");
             PropertyVersionsBuilder builder = builders.get(property.getName());
             if (builder == null || !builder.isAssociated()) {
-                getLog().debug("Property ${" + property.getName() + "}: Looks like this property is not "
+                log.debug("Property ${" + property.getName() + "}: Looks like this property is not "
                         + "associated with any dependency...");
-                builder = new PropertyVersionsBuilder(this, null, property.getName());
+                builder = new PropertyVersionsBuilder(this, null, property.getName(), log);
             }
             if (!property.isAutoLinkDependencies()) {
-                getLog().debug("Property ${" + property.getName() + "}: Removing any autoLinkDependencies");
+                log.debug("Property ${" + property.getName() + "}: Removing any autoLinkDependencies");
                 builder.clearAssociations();
             }
             Dependency[] dependencies = property.getDependencies();
             if (dependencies != null) {
                 for (Dependency dependency : dependencies) {
-                    getLog().debug("Property ${" + property.getName() + "}: Adding association to " + dependency);
+                    log.debug("Property ${" + property.getName() + "}: Adding association to " + dependency);
                     builder.withAssociation(artifactFactory.createArtifact(dependency), false);
                 }
             }
@@ -630,7 +630,7 @@ public class DefaultVersionsHelper implements VersionsHelper {
                 if (property.isAutoLinkDependencies()
                         && StringUtils.isEmpty(property.getVersion())
                         && !StringUtils.isEmpty(builder.getVersionRange())) {
-                    getLog().debug("Property ${" + property.getName() + "}: Adding inferred version range of "
+                    log.debug("Property ${" + property.getName() + "}: Adding inferred version range of "
                             + builder.getVersionRange());
                     property.setVersion(builder.getVersionRange());
                 }
@@ -660,14 +660,25 @@ public class DefaultVersionsHelper implements VersionsHelper {
      */
     public static class Builder {
         private Collection<String> ignoredVersions;
+
         private RuleSet ruleSet;
+
         private String serverId;
+
         private String rulesUri;
+
         private Log log;
+
         private MavenSession mavenSession;
+
         private MojoExecution mojoExecution;
+
         private RepositorySystem repositorySystem;
+
         private ArtifactFactory artifactFactory;
+
+        private PomHelper pomHelper;
+
         private Map<String, Wagon> wagonMap;
 
         public Builder() {}
@@ -917,14 +928,20 @@ public class DefaultVersionsHelper implements VersionsHelper {
             return this;
         }
 
+        public Builder withPomHelper(PomHelper pomHelper) {
+            this.pomHelper = pomHelper;
+            return this;
+        }
+
         /**
          * Builds the constructed {@linkplain DefaultVersionsHelper} object
+         *
          * @return constructed {@linkplain DefaultVersionsHelper}
          * @throws MojoExecutionException should the constructor with the RuleSet retrieval doesn't succeed
          */
         public DefaultVersionsHelper build() throws MojoExecutionException {
-            DefaultVersionsHelper instance =
-                    new DefaultVersionsHelper(artifactFactory, repositorySystem, mavenSession, mojoExecution, log);
+            DefaultVersionsHelper instance = new DefaultVersionsHelper(
+                    pomHelper, artifactFactory, repositorySystem, mavenSession, mojoExecution, log);
             if (ruleSet != null) {
                 if (!isBlank(rulesUri)) {
                     log.warn("rulesUri is ignored if rules are specified in pom or as parameters");
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/api/IgnoreVersionHelper.java b/versions-common/src/main/java/org/codehaus/mojo/versions/api/IgnoreVersionHelper.java
index 489a2273..494d3b9c 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/api/IgnoreVersionHelper.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/api/IgnoreVersionHelper.java
@@ -27,10 +27,9 @@ import org.apache.maven.artifact.versioning.ArtifactVersion;
 import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
 import org.apache.maven.artifact.versioning.VersionRange;
 import org.codehaus.mojo.versions.model.IgnoreVersion;
+import org.codehaus.mojo.versions.utils.ArtifactVersionService;
 import org.eclipse.aether.version.Version;
 
-import static org.codehaus.mojo.versions.utils.ArtifactVersionService.getArtifactVersion;
-
 /**
  * Helper class for {@link IgnoreVersion}
  */
@@ -82,7 +81,7 @@ public class IgnoreVersionHelper {
 
     private static boolean isVersionIgnoredRange(Version version, IgnoreVersion ignoreVersion) {
         try {
-            ArtifactVersion aVersion = getArtifactVersion(version.toString());
+            ArtifactVersion aVersion = ArtifactVersionService.getArtifactVersion(version.toString());
             VersionRange versionRange = VersionRange.createFromVersionSpec(ignoreVersion.getVersion());
             if (versionRange.hasRestrictions()) {
                 return versionRange.containsVersion(aVersion);
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/api/PomHelper.java b/versions-common/src/main/java/org/codehaus/mojo/versions/api/PomHelper.java
index bc560ab0..a59d3845 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/api/PomHelper.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/api/PomHelper.java
@@ -35,37 +35,42 @@ import java.io.UncheckedIOException;
 import java.nio.charset.Charset;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashMap;
+import java.util.Collections;
+import java.util.Deque;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Properties;
 import java.util.Set;
-import java.util.Stack;
 import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.function.Consumer;
+import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
 import org.apache.maven.artifact.versioning.VersionRange;
 import org.apache.maven.execution.MavenSession;
+import org.apache.maven.model.BuildBase;
 import org.apache.maven.model.Dependency;
+import org.apache.maven.model.DependencyManagement;
 import org.apache.maven.model.Model;
 import org.apache.maven.model.Parent;
 import org.apache.maven.model.Plugin;
 import org.apache.maven.model.Profile;
 import org.apache.maven.model.ReportPlugin;
+import org.apache.maven.model.Reporting;
 import org.apache.maven.model.building.ModelBuildingRequest;
 import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
 import org.apache.maven.plugin.logging.Log;
@@ -85,49 +90,60 @@ import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator
 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
 import org.codehaus.stax2.XMLInputFactory2;
 
-import static java.util.Collections.singletonMap;
 import static java.util.Optional.ofNullable;
 import static org.codehaus.mojo.versions.api.PomHelper.Marks.CHILD_START;
+import static org.codehaus.mojo.versions.api.PomHelper.Marks.END;
 import static org.codehaus.mojo.versions.api.PomHelper.Marks.END_ELEMENT;
 import static org.codehaus.mojo.versions.api.PomHelper.Marks.PARENT_START;
+import static org.codehaus.mojo.versions.api.PomHelper.Marks.START;
 
-/**
- * Helper class for modifying pom files.
- *
- * @author Stephen Connolly
- * @since 1.0-alpha-3
- */
 public class PomHelper {
     public static final String APACHE_MAVEN_PLUGINS_GROUPID = "org.apache.maven.plugins";
 
-    public static final Pattern PATTERN_PROJECT_PROPERTIES = Pattern.compile("/project/properties");
-
-    public static final Pattern PATTERN_PROJECT_PROFILE = Pattern.compile("/project/profiles/profile");
-
-    public static final Pattern PATTERN_PROJECT_PROFILE_ID = Pattern.compile("/project/profiles/profile/id");
+    private static final Set<String> IMPLICIT_PATHS = new HashSet<>(Arrays.asList(
+            "/project/parent/groupId", "/project/parent/artifactId",
+            "/project/parent/version", "/project/groupId",
+            "/project/artifactId", "/project/version"));
 
-    public static final Pattern PATTERN_PROJECT_VERSION = Pattern.compile("/project/version");
+    private static final Pattern PATTERN_PROJECT_VERSION = Pattern.compile("/project/version");
 
-    public static final Pattern PATTERN_PROJECT_PARENT_VERSION = Pattern.compile("/project/parent/version");
+    private static final Pattern PATTERN_PROJECT_PARENT_VERSION = Pattern.compile("/project/parent/version");
 
-    public static final Pattern PATTERN_PROJECT_DEPENDENCY = Pattern.compile("/project" + "(/profiles/profile)?"
+    private static final Pattern PATTERN_PROJECT_DEPENDENCY = Pattern.compile("/project" + "(/profiles/profile)?"
             + "((/dependencyManagement)|(/build(/pluginManagement)?/plugins/plugin))?"
             + "/dependencies/dependency");
 
-    public static final Pattern PATTERN_PROJECT_DEPENDENCY_VERSION = Pattern.compile("/project" + "(/profiles/profile)?"
-            + "((/dependencyManagement)|(/build(/pluginManagement)?/plugins/plugin))?"
-            + "/dependencies/dependency"
-            + "((/groupId)|(/artifactId)|(/version))");
+    private static final Pattern PATTERN_PROJECT_DEPENDENCY_VERSION =
+            Pattern.compile("/project" + "(/profiles/profile)?"
+                    + "((/dependencyManagement)|(/build(/pluginManagement)?/plugins/plugin))?"
+                    + "/dependencies/dependency"
+                    + "((/groupId)|(/artifactId)|(/version))");
 
-    public static final Pattern PATTERN_PROJECT_PLUGIN = Pattern.compile(
+    private static final Pattern PATTERN_PROJECT_PLUGIN = Pattern.compile(
             "/project" + "(/profiles/profile)?" + "((/build(/pluginManagement)?)|(/reporting))/plugins/plugin");
 
-    public static final Pattern PATTERN_PROJECT_PLUGIN_VERSION = Pattern.compile("/project" + "(/profiles/profile)?"
+    private static final Pattern PATTERN_PROJECT_PLUGIN_VERSION = Pattern.compile("/project" + "(/profiles/profile)?"
             + "((/build(/pluginManagement)?)|(/reporting))/plugins/plugin"
             + "((/groupId)|(/artifactId)|(/version))");
 
-    private PomHelper() {
-        // utility class
+    private final ArtifactFactory artifactFactory;
+
+    private final ExpressionEvaluator expressionEvaluator;
+
+    enum Marks {
+        CHILD_START,
+        PARENT_START,
+        END_ELEMENT,
+        START,
+        END
+    }
+
+    /**
+     * Creates a new instance
+     */
+    public PomHelper(ArtifactFactory artifactFactory, ExpressionEvaluator expressionEvaluator) {
+        this.artifactFactory = artifactFactory;
+        this.expressionEvaluator = expressionEvaluator;
     }
 
     /**
@@ -199,63 +215,60 @@ public class PomHelper {
      * @throws XMLStreamException if somethinh went wrong.
      */
     public static boolean setPropertyVersion(
-            final MutableXMLStreamReader pom, final String profileId, final String property, final String value)
-            throws XMLStreamException {
-        Stack<String> stack = new Stack<>();
-        String path = "";
-        final Pattern propertyRegex;
-        final Pattern matchScopeRegex;
-        final Pattern projectProfileId;
-        boolean inMatchScope = false;
-        boolean madeReplacement = false;
-        if (profileId == null) {
-            propertyRegex = Pattern.compile("/project/properties/" + RegexUtils.quote(property));
-            matchScopeRegex = PATTERN_PROJECT_PROPERTIES;
-            projectProfileId = null;
-        } else {
-            propertyRegex = Pattern.compile("/project/profiles/profile/properties/" + RegexUtils.quote(property));
-            matchScopeRegex = PATTERN_PROJECT_PROFILE;
-            projectProfileId = PATTERN_PROJECT_PROFILE_ID;
-        }
-
-        pom.rewind();
-        while (pom.hasNext()) {
-            pom.next();
-            if (pom.isStartElement()) {
-                stack.push(path);
-                path = path + "/" + pom.getLocalName();
-
-                if (propertyRegex.matcher(path).matches()) {
-                    pom.mark(0);
-                } else if (matchScopeRegex.matcher(path).matches()) {
-                    // we're in a new match scope
-                    // reset any previous partial matches
-                    inMatchScope = profileId == null;
-                    pom.clearMark(0);
-                    pom.clearMark(1);
-                } else if (profileId != null && projectProfileId.matcher(path).matches()) {
-                    String candidateId = pom.getElementText();
+            MutableXMLStreamReader pom, String profileId, String property, String value) throws XMLStreamException {
+        class PropertyVersionInternal {
+            final Pattern propertyRegex = Pattern.compile(
+                    (profileId == null ? "/project/properties/" : "/project/profiles/profile/properties/")
+                            + RegexUtils.quote(property));
+            final Pattern matchScopeRegex = profileId == null
+                    ? Pattern.compile("/project/properties")
+                    : Pattern.compile("/project/profiles/profile");
+            final Pattern profileIdRegex = profileId == null ? null : Pattern.compile("/project/profiles/profile/id");
+
+            boolean setPropertyVersion(String path, boolean inMatchScope) throws XMLStreamException {
+                boolean replaced = false;
+                while (!replaced && pom.hasNext()) {
+                    pom.next();
+                    if (pom.isStartElement()) {
+                        String elementPath = path + "/" + pom.getLocalName();
+                        if (propertyRegex.matcher(elementPath).matches()) {
+                            pom.mark(START);
+                        } else if (matchScopeRegex.matcher(elementPath).matches()) {
+                            // we're in a new match scope -> reset any previous partial matches
+                            inMatchScope = profileId == null;
+                            pom.clearMark(START);
+                            pom.clearMark(END);
+                        } else if (profileId != null
+                                && profileIdRegex.matcher(elementPath).matches()) {
+                            inMatchScope =
+                                    profileId.trim().equals(pom.getElementText().trim());
+                        }
 
-                    inMatchScope = profileId.trim().equals(candidateId.trim());
-                }
-            }
-            // for empty elements, pom can be both start- and end element
-            if (pom.isEndElement()) {
-                if (propertyRegex.matcher(path).matches()) {
-                    pom.mark(1);
-                } else if (matchScopeRegex.matcher(path).matches()) {
-                    if (inMatchScope && pom.hasMark(0) && pom.hasMark(1)) {
-                        pom.replaceBetween(0, 1, value);
-                        madeReplacement = true;
+                        // getElementText could've pushed the pointer to END_ELEMENT
+                        if (!pom.isEndElement()) {
+                            // inMatchScope will not change in the child element
+                            replaced = setPropertyVersion(elementPath, inMatchScope);
+                        }
+                    } else if (pom.isEndElement()) {
+                        if (propertyRegex.matcher(path).matches()) {
+                            pom.mark(END);
+                        } else if (matchScopeRegex.matcher(path).matches()) {
+                            if (inMatchScope && pom.hasMark(START) && pom.hasMark(END)) {
+                                pom.replaceBetween(START, END, value);
+                                replaced = true;
+                            }
+                            pom.clearMark(START);
+                            pom.clearMark(END);
+                        }
+                        return replaced;
                     }
-                    pom.clearMark(0);
-                    pom.clearMark(1);
-                    inMatchScope = false;
                 }
-                path = stack.pop();
+                return replaced;
             }
         }
-        return madeReplacement;
+
+        pom.rewind();
+        return new PropertyVersionInternal().setPropertyVersion("", false);
     }
 
     /**
@@ -289,12 +302,6 @@ public class PomHelper {
         return setElementValue(pom, parentPath, elementName, value, true);
     }
 
-    enum Marks {
-        CHILD_START,
-        PARENT_START,
-        END_ELEMENT
-    }
-
     /**
      * Sets the value of the given element given its parent element path.
      * Will only consider the first found occurrence of the parent element.
@@ -382,6 +389,33 @@ public class PomHelper {
         return new ElementValueInternal().process("");
     }
 
+    static <T> T executeOnPatternFound(
+            String path, final MutableXMLStreamReader pom, Pattern pattern, Supplier<T> onPatternFound)
+            throws XMLStreamException {
+        T result = null;
+        while (result == null && pom.hasNext()) {
+            pom.next();
+            if (pom.isStartElement()) {
+                String elementPath = path + "/" + pom.getLocalName();
+                if (pattern.matcher(elementPath).matches()) {
+                    pom.mark(START);
+                }
+                result = executeOnPatternFound(elementPath, pom, pattern, onPatternFound);
+            } else if (pom.isEndElement()) {
+                if (pattern.matcher(path).matches()) {
+                    pom.mark(END);
+                    if (pom.hasMark(START)) {
+                        return onPatternFound.get();
+                    }
+                    pom.clearMark(START);
+                    pom.clearMark(END);
+                }
+                break;
+            }
+        }
+        return result;
+    }
+
     /**
      * Retrieves the project version from the pom.
      *
@@ -391,34 +425,9 @@ public class PomHelper {
      * @throws XMLStreamException if something went wrong.
      */
     public static String getProjectVersion(final MutableXMLStreamReader pom) throws XMLStreamException {
-        Stack<String> stack = new Stack<>();
-        String path = "";
-
         pom.rewind();
-        while (pom.hasNext()) {
-            pom.next();
-            if (pom.isStartElement()) {
-                stack.push(path);
-                path = path + "/" + pom.getLocalName();
-
-                if (PATTERN_PROJECT_VERSION.matcher(path).matches()) {
-                    pom.mark(0);
-                }
-            }
-            // for empty elements, pom can be both start- and end element
-            if (pom.isEndElement()) {
-                if (PATTERN_PROJECT_VERSION.matcher(path).matches()) {
-                    pom.mark(1);
-                    if (pom.hasMark(0) && pom.hasMark(1)) {
-                        return pom.getBetween(0, 1).trim();
-                    }
-                    pom.clearMark(0);
-                    pom.clearMark(1);
-                }
-                path = stack.pop();
-            }
-        }
-        return null;
+        return executeOnPatternFound("", pom, PATTERN_PROJECT_VERSION, () -> pom.getBetween(START, END)
+                .trim());
     }
 
     /**
@@ -431,36 +440,12 @@ public class PomHelper {
      */
     public static boolean setProjectParentVersion(final MutableXMLStreamReader pom, final String value)
             throws XMLStreamException {
-        Stack<String> stack = new Stack<>();
-        String path = "";
-        boolean madeReplacement = false;
-
         pom.rewind();
-        while (pom.hasNext()) {
-            pom.next();
-            if (pom.isStartElement()) {
-                stack.push(path);
-                path = path + "/" + pom.getLocalName();
-
-                if (PATTERN_PROJECT_PARENT_VERSION.matcher(path).matches()) {
-                    pom.mark(0);
-                }
-            }
-            // for empty elements, pom can be both start- and end element
-            if (pom.isEndElement()) {
-                if (PATTERN_PROJECT_PARENT_VERSION.matcher(path).matches()) {
-                    pom.mark(1);
-                    if (pom.hasMark(0) && pom.hasMark(1)) {
-                        pom.replaceBetween(0, 1, value);
-                        madeReplacement = true;
-                    }
-                    pom.clearMark(0);
-                    pom.clearMark(1);
-                }
-                path = stack.pop();
-            }
-        }
-        return madeReplacement;
+        return ofNullable(executeOnPatternFound("", pom, PATTERN_PROJECT_PARENT_VERSION, () -> {
+                    pom.replaceBetween(START, END, value);
+                    return true;
+                }))
+                .orElse(false);
     }
 
     /**
@@ -486,52 +471,9 @@ public class PomHelper {
             final Model model,
             final Log logger)
             throws XMLStreamException {
-        Stack<String> stack = new Stack<>();
-        String path = "";
-
-        Set<String> implicitPaths = new HashSet<>(Arrays.asList(
-                "/project/parent/groupId", "/project/parent/artifactId",
-                "/project/parent/version", "/project/groupId",
-                "/project/artifactId", "/project/version"));
-        Map<String, String> implicitProperties = new HashMap<>();
+        Map<String, String> implicitProperties = getImplicitProperties(pom, model);
 
-        for (Map.Entry<Object, Object> entry : model.getProperties().entrySet()) {
-            implicitProperties.put((String) entry.getKey(), (String) entry.getValue());
-        }
-
-        pom.rewind();
-        while (pom.hasNext()) {
-            pom.next();
-            if (pom.isStartElement()) {
-                stack.push(path);
-                path = path + "/" + pom.getLocalName();
-
-                if (implicitPaths.contains(path)) {
-                    final String elementText = pom.getElementText().trim();
-                    implicitProperties.put(path.substring(1).replace('/', '.'), elementText);
-                }
-            }
-            if (pom.isEndElement()) {
-                path = stack.pop();
-            }
-        }
-
-        for (boolean modified = true; modified; ) {
-            modified = false;
-            for (Map.Entry<String, String> entry : implicitProperties.entrySet()) {
-                if (entry.getKey().contains(".parent")) {
-                    String child = entry.getKey().replace(".parent", "");
-                    if (!implicitProperties.containsKey(child)) {
-                        implicitProperties.put(child, entry.getValue());
-                        modified = true;
-                        break;
-                    }
-                }
-            }
-        }
-
-        stack = new Stack<>();
-        path = "";
+        String path = "";
         boolean inMatchScope = false;
         boolean madeReplacement = false;
         boolean haveGroupId = false;
@@ -539,18 +481,17 @@ public class PomHelper {
         boolean haveOldVersion = false;
 
         pom.rewind();
-        while (pom.hasNext()) {
+        for (Deque<String> stack = new ArrayDeque<>(); pom.hasNext(); ) {
             pom.next();
             if (pom.isStartElement()) {
                 stack.push(path);
                 path = path + "/" + pom.getLocalName();
-
                 if (PATTERN_PROJECT_DEPENDENCY.matcher(path).matches()) {
                     // we're in a new match scope
                     // reset any previous partial matches
                     inMatchScope = true;
-                    pom.clearMark(0);
-                    pom.clearMark(1);
+                    pom.clearMark(START);
+                    pom.clearMark(END);
 
                     haveGroupId = false;
                     haveArtifactId = false;
@@ -564,7 +505,7 @@ public class PomHelper {
                         haveArtifactId =
                                 artifactId.equals(evaluate(pom.getElementText().trim(), implicitProperties, logger));
                     } else if ("version".equals(pom.getLocalName())) {
-                        pom.mark(0);
+                        pom.mark(START);
                     }
                 }
             }
@@ -572,9 +513,9 @@ public class PomHelper {
             if (pom.isEndElement()) {
                 if (PATTERN_PROJECT_DEPENDENCY_VERSION.matcher(path).matches()
                         && "version".equals(pom.getLocalName())) {
-                    pom.mark(1);
-                    String compressedPomVersion =
-                            StringUtils.deleteWhitespace(pom.getBetween(0, 1).trim());
+                    pom.mark(END);
+                    String compressedPomVersion = StringUtils.deleteWhitespace(
+                            pom.getBetween(START, END).trim());
                     String compressedOldVersion = StringUtils.deleteWhitespace(oldVersion);
 
                     try {
@@ -585,16 +526,16 @@ public class PomHelper {
                     }
                 } else if (PATTERN_PROJECT_DEPENDENCY.matcher(path).matches()) {
                     if (inMatchScope
-                            && pom.hasMark(0)
-                            && pom.hasMark(1)
+                            && pom.hasMark(START)
+                            && pom.hasMark(END)
                             && haveGroupId
                             && haveArtifactId
                             && haveOldVersion) {
-                        pom.replaceBetween(0, 1, newVersion);
+                        pom.replaceBetween(START, END, newVersion);
                         madeReplacement = true;
                     }
-                    pom.clearMark(0);
-                    pom.clearMark(1);
+                    pom.clearMark(START);
+                    pom.clearMark(END);
                     haveArtifactId = false;
                     haveGroupId = false;
                     haveOldVersion = false;
@@ -606,6 +547,43 @@ public class PomHelper {
         return madeReplacement;
     }
 
+    static Map<String, String> getImplicitProperties(MutableXMLStreamReader pom, Model model)
+            throws XMLStreamException {
+
+        Map<String, String> implicitProperties = model.getProperties().entrySet().stream()
+                .collect(Collectors.toMap(e -> (String) e.getKey(), e -> (String) e.getValue()));
+
+        pom.rewind();
+        getImplicitProperties("", pom, implicitProperties);
+
+        List<Pair<String, String>> parentProperties = implicitProperties.entrySet().stream()
+                .filter(e -> e.getKey().contains(".parent"))
+                .map(e -> Pair.of(e.getKey().replace(".parent", ""), e.getValue()))
+                .collect(Collectors.toList());
+        parentProperties.forEach(p -> implicitProperties.putIfAbsent(p.getLeft(), p.getRight()));
+        return implicitProperties;
+    }
+
+    private static void getImplicitProperties(String path, MutableXMLStreamReader pom, Map<String, String> acc)
+            throws XMLStreamException {
+        while (pom.hasNext()) {
+            pom.next();
+            if (pom.isStartElement()) {
+                String elementPath = path + "/" + pom.getLocalName();
+                if (IMPLICIT_PATHS.contains(elementPath)) {
+                    final String elementText = pom.getElementText().trim();
+                    acc.put(elementPath.substring(1).replace('/', '.'), elementText);
+                    // getElementText pushed the pointer to END_ELEMENT, we should NOT descend
+                } else {
+                    // only descend if state != END_ELEMENT at this point
+                    getImplicitProperties(elementPath, pom, acc);
+                }
+            } else if (pom.isEndElement()) {
+                return;
+            }
+        }
+    }
+
     /**
      * A lightweight expression evaluation function.
      *
@@ -622,17 +600,11 @@ public class PomHelper {
         return extractExpression(expr)
                 .map(expression -> {
                     String value = properties.get(expression);
-
                     if (value != null) {
                         int exprStartDelimiter = value.indexOf("${");
-
                         if (exprStartDelimiter >= 0) {
-                            if (exprStartDelimiter > 0) {
-                                value = value.substring(0, exprStartDelimiter)
-                                        + evaluate(value.substring(exprStartDelimiter), properties, logger);
-                            } else {
-                                value = evaluate(value.substring(exprStartDelimiter), properties, logger);
-                            }
+                            value = value.substring(0, exprStartDelimiter)
+                                    + evaluate(value.substring(exprStartDelimiter), properties, logger);
                         }
                     } else {
                         // Because we work with the raw model, without interpolation, unevaluatable expressions are not
@@ -646,25 +618,16 @@ public class PomHelper {
                     if (index >= 0) {
                         int lastIndex = expr.indexOf("}", index);
                         if (lastIndex >= 0) {
-                            String retVal = expr.substring(0, index);
-
+                            String retVal = expr.substring(0, index)
+                                    + evaluate(expr.substring(index, lastIndex + 1), properties, logger)
+                                    + evaluate(expr.substring(lastIndex + 1), properties, logger);
                             if (index > 0 && expr.charAt(index - 1) == '$') {
-                                retVal += expr.substring(index + 1, lastIndex + 1);
-                            } else {
-                                retVal += evaluate(expr.substring(index, lastIndex + 1), properties, logger);
+                                retVal = expr.substring(0, index - 1) + retVal;
                             }
-
-                            retVal += evaluate(expr.substring(lastIndex + 1), properties, logger);
                             return retVal;
                         }
                     }
-
-                    // Was not an expression
-                    if (expr.contains("$$")) {
-                        return expr.replaceAll("\\$\\$", "\\$");
-                    } else {
-                        return expr;
-                    }
+                    return expr.contains("$$") ? expr.replaceAll("\\$\\$", "\\$") : expr;
                 });
     }
 
@@ -690,7 +653,7 @@ public class PomHelper {
      * @return true if both versions have an overlap
      * @throws InvalidVersionSpecificationException if the versions can't be parsed to a range
      */
-    public static boolean isVersionOverlap(String leftVersionOrRange, String rightVersionOrRange)
+    static boolean isVersionOverlap(String leftVersionOrRange, String rightVersionOrRange)
             throws InvalidVersionSpecificationException {
         VersionRange pomVersionRange = createVersionRange(leftVersionOrRange);
         if (!pomVersionRange.hasRestrictions()) {
@@ -732,7 +695,7 @@ public class PomHelper {
             final String oldVersion,
             final String newVersion)
             throws XMLStreamException {
-        Stack<String> stack = new Stack<>();
+        Deque<String> stack = new ArrayDeque<>();
         String path = "";
         boolean inMatchScope = false;
         boolean madeReplacement = false;
@@ -753,8 +716,8 @@ public class PomHelper {
                     // we're in a new match scope
                     // reset any previous partial matches
                     inMatchScope = true;
-                    pom.clearMark(0);
-                    pom.clearMark(1);
+                    pom.clearMark(START);
+                    pom.clearMark(END);
 
                     haveGroupId = false;
                     haveArtifactId = false;
@@ -766,33 +729,34 @@ public class PomHelper {
                     } else if ("artifactId".equals(elementName)) {
                         haveArtifactId = artifactId.equals(pom.getElementText().trim());
                     } else if ("version".equals(elementName)) {
-                        pom.mark(0);
+                        pom.mark(START);
                     }
                 }
             }
             // for empty elements, pom can be both start- and end element
             if (pom.isEndElement()) {
                 if (PATTERN_PROJECT_PLUGIN_VERSION.matcher(path).matches() && "version".equals(pom.getLocalName())) {
-                    pom.mark(1);
+                    pom.mark(END);
 
                     try {
                         haveOldVersion = isVersionOverlap(
-                                oldVersion, pom.getBetween(0, 1).trim());
+                                oldVersion, pom.getBetween(START, END).trim());
                     } catch (InvalidVersionSpecificationException e) {
                         // fall back to string comparison
-                        haveOldVersion = oldVersion.equals(pom.getBetween(0, 1).trim());
+                        haveOldVersion =
+                                oldVersion.equals(pom.getBetween(START, END).trim());
                     }
                 } else if (PATTERN_PROJECT_PLUGIN.matcher(path).matches()) {
                     if (inMatchScope
-                            && pom.hasMark(0)
-                            && pom.hasMark(1)
+                            && pom.hasMark(START)
+                            && pom.hasMark(END)
                             && (haveGroupId || !needGroupId)
                             && haveArtifactId
                             && haveOldVersion) {
-                        pom.replaceBetween(0, 1, newVersion);
+                        pom.replaceBetween(START, END, newVersion);
                         madeReplacement = true;
-                        pom.clearMark(0);
-                        pom.clearMark(1);
+                        pom.clearMark(START);
+                        pom.clearMark(END);
                         haveArtifactId = false;
                         haveGroupId = false;
                         haveOldVersion = false;
@@ -811,7 +775,7 @@ public class PomHelper {
      * @param project maven project of the child for which the models need to be gathered
      * @return gathered map of raw models per project
      */
-    private static Map<MavenProject, Model> getRawModelWithParents(MavenProject project) throws IOException {
+    static Map<MavenProject, Model> getRawModelWithParents(MavenProject project) throws IOException {
         // constructs a tree sorted from children to parents
         Map<MavenProject, Model> models = new TreeMap<>((p1, p2) -> {
             for (MavenProject p = p1; p != null; p = p.getParent()) {
@@ -831,7 +795,7 @@ public class PomHelper {
     /**
      * Examines the project to find any properties which are associated with versions of artifacts in the project.
      *
-     * @param helper        Our versions helper.
+     * @param helper        {@link VersionsHelper} instance
      * @param project       The project to examine.
      * @param includeParent whether parent POMs should be included
      * @return An array of properties that are associated within the project.
@@ -839,165 +803,118 @@ public class PomHelper {
      * @throws IOException                   if the project's pom file cannot be parsed.
      * @since 1.0-alpha-3
      */
-    public static PropertyVersionsBuilder[] getPropertyVersionsBuilders(
-            VersionsHelper helper, ArtifactFactory artifactFactory, MavenProject project, boolean includeParent)
+    public PropertyVersionsBuilder[] getPropertyVersionsBuilders(
+            VersionsHelper helper, Log log, MavenProject project, boolean includeParent)
             throws ExpressionEvaluationException, IOException {
-        ExpressionEvaluator expressionEvaluator = helper.getExpressionEvaluator(project);
-        Map<MavenProject, Model> reactorModels =
-                includeParent ? getRawModelWithParents(project) : singletonMap(project, getRawModel(project));
+
+        Map<MavenProject, Model> reactorModels = includeParent
+                ? getRawModelWithParents(project)
+                : Collections.singletonMap(project, getRawModel(project));
+
+        Set<String> activeProfileIds =
+                project.getActiveProfiles().stream().map(Profile::getId).collect(Collectors.toSet());
 
         Map<String, PropertyVersionsBuilder> propertiesMap = new TreeMap<>();
+        reactorModels.values().forEach(model -> {
+            processProfiles(helper, log, propertiesMap, model, activeProfileIds);
+            putPropertiesIfAbsent(helper, log, propertiesMap, null, model.getProperties());
+        });
 
-        Set<String> activeProfiles = new TreeSet<>();
-        for (Profile profile : project.getActiveProfiles()) {
-            activeProfiles.add(profile.getId());
+        // Process the project and its parent hierarchy
+        for (MavenProject currentProject = project;
+                currentProject != null;
+                currentProject = includeParent ? currentProject.getParent() : null) {
+
+            Model model = reactorModels.get(currentProject);
+            if (model != null) {
+                processModel(propertiesMap, model, activeProfileIds);
+            }
         }
 
-        // add any properties from profiles first (as they override properties from the project
-        for (Iterator<Profile> it = reactorModels.values().stream()
-                        .flatMap(model -> model.getProfiles().stream())
-                        .filter(profile -> activeProfiles.contains(profile.getId()))
-                        .iterator();
-                it.hasNext(); ) {
-            Profile profile = it.next();
-            try {
-                addProperties(helper, propertiesMap, profile.getId(), profile.getProperties());
-                if (profile.getDependencyManagement() != null) {
-                    addDependencyAssocations(
-                            artifactFactory,
-                            expressionEvaluator,
-                            propertiesMap,
-                            profile.getDependencyManagement().getDependencies(),
-                            false);
-                }
-                addDependencyAssocations(
-                        artifactFactory, expressionEvaluator, propertiesMap, profile.getDependencies(), false);
-                if (profile.getBuild() != null) {
-                    if (profile.getBuild().getPluginManagement() != null) {
-                        addPluginAssociations(
-                                artifactFactory,
-                                expressionEvaluator,
-                                propertiesMap,
-                                profile.getBuild().getPluginManagement().getPlugins());
+        // Finally, remove any properties without associations
+        propertiesMap.values().removeIf(versions -> !versions.isAssociated());
+
+        return propertiesMap.values().toArray(new PropertyVersionsBuilder[0]);
+    }
+
+    private void processProfiles(
+            VersionsHelper helper,
+            Log log,
+            Map<String, PropertyVersionsBuilder> propertiesMap,
+            Model model,
+            Set<String> activeProfileIds) {
+
+        model.getProfiles().stream()
+                .filter(profile -> activeProfileIds.contains(profile.getId()))
+                .forEach(profile -> {
+                    try {
+                        putPropertiesIfAbsent(helper, log, propertiesMap, profile.getId(), profile.getProperties());
+                        processDependencies(
+                                propertiesMap, profile.getDependencyManagement(), profile.getDependencies());
+                        processBuild(propertiesMap, profile.getBuild());
+                        processReporting(propertiesMap, profile.getReporting());
+                    } catch (ExpressionEvaluationException e) {
+                        throw new RuntimeException(e);
                     }
-                    addPluginAssociations(
-                            artifactFactory,
-                            expressionEvaluator,
-                            propertiesMap,
-                            profile.getBuild().getPlugins());
-                }
-                if (profile.getReporting() != null) {
-                    addReportPluginAssociations(
-                            artifactFactory,
-                            expressionEvaluator,
-                            propertiesMap,
-                            profile.getReporting().getPlugins());
-                }
-            } catch (ExpressionEvaluationException e) {
-                throw new RuntimeException(e);
+                });
+    }
+
+    private void processModel(
+            Map<String, PropertyVersionsBuilder> propertiesMap, Model model, Set<String> activeProfileIds)
+            throws ExpressionEvaluationException {
+
+        processDependencies(propertiesMap, model.getDependencyManagement(), model.getDependencies());
+        processBuild(propertiesMap, model.getBuild());
+        processReporting(propertiesMap, model.getReporting());
+
+        // Process active profiles within the model
+        for (Profile profile : model.getProfiles()) {
+            if (activeProfileIds.contains(profile.getId())) {
+                processDependencies(propertiesMap, profile.getDependencyManagement(), profile.getDependencies());
             }
         }
+    }
 
-        // second, we add all the properties in the pom
-        reactorModels.values().forEach(model -> addProperties(helper, propertiesMap, null, model.getProperties()));
-
-        for (MavenProject currentPrj = project;
-                currentPrj != null;
-                currentPrj = includeParent ? currentPrj.getParent() : null) {
-            Model model = reactorModels.get(currentPrj);
-
-            if (model.getDependencyManagement() != null) {
-                addDependencyAssocations(
-                        artifactFactory,
-                        expressionEvaluator,
-                        propertiesMap,
-                        model.getDependencyManagement().getDependencies(),
-                        false);
-            }
-            addDependencyAssocations(
-                    artifactFactory, expressionEvaluator, propertiesMap, model.getDependencies(), false);
-            if (model.getBuild() != null) {
-                if (model.getBuild().getPluginManagement() != null) {
-                    addPluginAssociations(
-                            artifactFactory,
-                            expressionEvaluator,
-                            propertiesMap,
-                            model.getBuild().getPluginManagement().getPlugins());
-                }
-                addPluginAssociations(
-                        artifactFactory,
-                        expressionEvaluator,
-                        propertiesMap,
-                        model.getBuild().getPlugins());
-            }
-            if (model.getReporting() != null) {
-                addReportPluginAssociations(
-                        artifactFactory,
-                        expressionEvaluator,
-                        propertiesMap,
-                        model.getReporting().getPlugins());
-            }
+    private void processDependencies(
+            Map<String, PropertyVersionsBuilder> propertiesMap,
+            DependencyManagement depMgmt,
+            List<Dependency> dependencies)
+            throws ExpressionEvaluationException {
+        if (depMgmt != null) {
+            addDependencyAssocations(propertiesMap, depMgmt.getDependencies(), false);
+        }
+        addDependencyAssocations(propertiesMap, dependencies, false);
+    }
 
-            // third, we add any associations from the active profiles
-            for (Profile profile : model.getProfiles()) {
-                if (!activeProfiles.contains(profile.getId())) {
-                    continue;
-                }
-                if (profile.getDependencyManagement() != null) {
-                    addDependencyAssocations(
-                            artifactFactory,
-                            expressionEvaluator,
-                            propertiesMap,
-                            profile.getDependencyManagement().getDependencies(),
-                            false);
-                }
-                addDependencyAssocations(
-                        artifactFactory, expressionEvaluator, propertiesMap, profile.getDependencies(), false);
-                if (profile.getBuild() != null) {
-                    if (profile.getBuild().getPluginManagement() != null) {
-                        addPluginAssociations(
-                                artifactFactory,
-                                expressionEvaluator,
-                                propertiesMap,
-                                profile.getBuild().getPluginManagement().getPlugins());
-                    }
-                    addPluginAssociations(
-                            artifactFactory,
-                            expressionEvaluator,
-                            propertiesMap,
-                            profile.getBuild().getPlugins());
-                }
-                if (profile.getReporting() != null) {
-                    addReportPluginAssociations(
-                            artifactFactory,
-                            expressionEvaluator,
-                            propertiesMap,
-                            profile.getReporting().getPlugins());
-                }
+    private void processBuild(Map<String, PropertyVersionsBuilder> propertiesMap, BuildBase build)
+            throws ExpressionEvaluationException {
+
+        if (build != null) {
+            if (build.getPluginManagement() != null) {
+                addPluginAssociations(propertiesMap, build.getPluginManagement().getPlugins());
             }
+            addPluginAssociations(propertiesMap, build.getPlugins());
         }
+    }
 
-        // finally, remove any properties without associations
-        purgeProperties(propertiesMap);
+    private void processReporting(Map<String, PropertyVersionsBuilder> propertiesMap, Reporting reporting)
+            throws ExpressionEvaluationException {
 
-        return propertiesMap.values().toArray(new PropertyVersionsBuilder[0]);
+        if (reporting != null) {
+            addReportPluginAssociations(propertiesMap, reporting.getPlugins());
+        }
     }
 
     /**
      * Takes a list of {@link org.apache.maven.model.Plugin} instances and adds associations to properties used to
      * define versions of the plugin artifact or any of the plugin dependencies specified in the pom.
      *
-     * @param expressionEvaluator Our expression evaluator.
      * @param result              The map of {@link org.codehaus.mojo.versions.api.PropertyVersionsBuilder} keyed by
      *                            property name.
      * @param plugins             The list of {@link org.apache.maven.model.Plugin}.
      * @throws ExpressionEvaluationException if an expression cannot be evaluated.
      */
-    private static void addPluginAssociations(
-            ArtifactFactory artifactFactory,
-            ExpressionEvaluator expressionEvaluator,
-            Map<String, PropertyVersionsBuilder> result,
-            List<Plugin> plugins)
+    private void addPluginAssociations(Map<String, PropertyVersionsBuilder> result, List<Plugin> plugins)
             throws ExpressionEvaluationException {
         if (plugins == null) {
             return;
@@ -1034,15 +951,12 @@ public class PomHelper {
                     }
                 }
             }
-            addDependencyAssocations(artifactFactory, expressionEvaluator, result, plugin.getDependencies(), true);
+            addDependencyAssocations(result, plugin.getDependencies(), true);
         }
     }
 
-    private static void addReportPluginAssociations(
-            ArtifactFactory artifactFactory,
-            ExpressionEvaluator expressionEvaluator,
-            Map<String, PropertyVersionsBuilder> result,
-            List<ReportPlugin> reportPlugins)
+    private void addReportPluginAssociations(
+            Map<String, PropertyVersionsBuilder> result, List<ReportPlugin> reportPlugins)
             throws ExpressionEvaluationException {
         if (reportPlugins == null) {
             return;
@@ -1082,12 +996,8 @@ public class PomHelper {
         }
     }
 
-    private static void addDependencyAssocations(
-            ArtifactFactory artifactFactory,
-            ExpressionEvaluator expressionEvaluator,
-            Map<String, PropertyVersionsBuilder> result,
-            List<Dependency> dependencies,
-            boolean usePluginRepositories)
+    private void addDependencyAssocations(
+            Map<String, PropertyVersionsBuilder> result, List<Dependency> dependencies, boolean usePluginRepositories)
             throws ExpressionEvaluationException {
         if (dependencies == null) {
             return;
@@ -1156,23 +1066,16 @@ public class PomHelper {
         }
     }
 
-    private static void addProperties(
+    private void putPropertiesIfAbsent(
             VersionsHelper helper,
+            Log log,
             Map<String, PropertyVersionsBuilder> result,
             String profileId,
             Properties properties) {
-        if (properties == null) {
-            return;
-        }
-        for (String propertyName : properties.stringPropertyNames()) {
-            if (!result.containsKey(propertyName)) {
-                result.put(propertyName, new PropertyVersionsBuilder(helper, profileId, propertyName));
-            }
-        }
-    }
-
-    private static void purgeProperties(Map<String, PropertyVersionsBuilder> result) {
-        result.values().removeIf(versions -> versions.getAssociations().length == 0);
+        ofNullable(properties)
+                .map(Properties::stringPropertyNames)
+                .ifPresent(propertyNames -> propertyNames.forEach(propertyName -> result.putIfAbsent(
+                        propertyName, new PropertyVersionsBuilder(helper, profileId, propertyName, log))));
     }
 
     /**
@@ -1197,8 +1100,10 @@ public class PomHelper {
      */
     public static Set<String> getAllChildModules(Model model, Log logger) {
         logger.debug("Finding child modules of " + model);
-        Set<String> childModules = new TreeSet<>(model.getModules());
-        model.getProfiles().forEach(profile -> childModules.addAll(profile.getModules()));
+        Set<String> childModules = Stream.concat(
+                        model.getModules().stream(),
+                        model.getProfiles().stream().flatMap(profile -> profile.getModules().stream()))
+                .collect(TreeSet::new, Set::add, Set::addAll);
         debugModules(logger, "Child modules:", childModules);
         return childModules;
     }
@@ -1228,11 +1133,8 @@ public class PomHelper {
      * @return The version.
      */
     public static String getVersion(Model model) {
-        String targetVersion = model.getVersion();
-        if (targetVersion == null && model.getParent() != null) {
-            targetVersion = model.getParent().getVersion();
-        }
-        return targetVersion;
+        return ofNullable(model.getVersion())
+                .orElse(ofNullable(model.getParent()).map(Parent::getVersion).orElse(null));
     }
 
     /**
@@ -1252,11 +1154,8 @@ public class PomHelper {
      * @return The artifactId.
      */
     public static String getArtifactId(Model model) {
-        String sourceArtifactId = model.getArtifactId();
-        if (sourceArtifactId == null && model.getParent() != null) {
-            sourceArtifactId = model.getParent().getArtifactId();
-        }
-        return sourceArtifactId;
+        return ofNullable(model.getArtifactId())
+                .orElse(ofNullable(model.getParent()).map(Parent::getArtifactId).orElse(null));
     }
 
     /**
@@ -1266,11 +1165,8 @@ public class PomHelper {
      * @return The groupId.
      */
     public static String getGroupId(Model model) {
-        String targetGroupId = model.getGroupId();
-        if (targetGroupId == null && model.getParent() != null) {
-            targetGroupId = model.getParent().getGroupId();
-        }
-        return targetGroupId;
+        return ofNullable(model.getGroupId())
+                .orElse(ofNullable(model.getParent()).map(Parent::getGroupId).orElse(null));
     }
 
     /**
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/api/Property.java b/versions-common/src/main/java/org/codehaus/mojo/versions/api/Property.java
index e7263939..535fd326 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/api/Property.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/api/Property.java
@@ -78,11 +78,9 @@ public class Property {
     private boolean searchReactor;
 
     /**
-     * When {@link #searchReactor} is <code>true</code> and a property version can be entirely satisfied from the
-     * reactor and this setting is <code>true</code> then the reactor version will be specified irrespective of any
-     * other settings (including {@link #banSnapshots}).
-     *
-     * @parameter default-value="true"
+     * When {@link #searchReactor} is {@code true} and a property version can be entirely satisfied from the
+     * reactor and this setting is {@code true} then the reactor version will be specified irrespective of any
+     * other settings (including {@link #isBanSnapshots}).
      * @since 1.0-alpha-3
      */
     private boolean preferReactor;
@@ -97,10 +95,17 @@ public class Property {
 
     private static final Dependency[] EMPTY_DEPENDENCY_ARRAY = new Dependency[0];
 
+    /**
+     * Creates a new instance without any set name
+     */
     public Property() {
         this.autoLinkDependencies = true;
     }
 
+    /**
+     * Creates a new {@link Property} with the given name
+     * @param name name of the property
+     */
     public Property(String name) {
         this.name = name;
         this.autoLinkDependencies = true;
@@ -112,46 +117,86 @@ public class Property {
         this.value = null;
     }
 
+    /**
+     * @return name
+     */
     public String getName() {
         return name;
     }
 
+    /**
+     * @param name name to be set
+     */
     public void setName(String name) {
         this.name = name;
     }
 
+    /**
+     * @return version
+     */
     public String getVersion() {
         return version;
     }
 
+    /**
+     * @param version version to be set
+     */
     public void setVersion(String version) {
         this.version = version;
     }
 
+    /**
+     * @return if properties linking versions should be auto-detected or not
+     */
     public boolean isAutoLinkDependencies() {
         return autoLinkDependencies;
     }
 
+    /**
+     * @param autoLinkDependencies whether properties indicating dependency versions should
+     *                             be linked automatically
+     */
     public void setAutoLinkDependencies(boolean autoLinkDependencies) {
         this.autoLinkDependencies = autoLinkDependencies;
     }
 
+    /**
+     * Dependencies that must be available for this property. If {@link #isAutoLinkDependencies} is {@code true} then
+     * these dependencies will be considered in addition to the automatically linked dependencies.
+     * @return dependencies
+     */
     public Dependency[] getDependencies() {
         return dependencies;
     }
 
+    /**
+     * Dependencies that must be available for this property. If {@link #isAutoLinkDependencies} is {@code true} then
+     * these dependencies will be considered in addition to the automatically linked dependencies.
+     * @param dependencies to be set
+     */
     public void setDependencies(Dependency[] dependencies) {
         this.dependencies = dependencies;
     }
 
+    /**
+     * @return whether reactor should be searched for the artifact version
+     */
     public boolean isSearchReactor() {
         return searchReactor;
     }
 
+    /**
+     * @param searchReactor whether reactor should be searched for the artifact version
+     */
     public void setSearchReactor(boolean searchReactor) {
         this.searchReactor = searchReactor;
     }
 
+    /**
+     * @return when {@link #searchReactor} is {@code true} and a property version can be entirely satisfied from the
+     * reactor and this setting is {@code true} then the reactor version will be specified irrespective of any
+     * other settings (including {@link #isBanSnapshots}).
+     */
     public boolean isPreferReactor() {
         return preferReactor;
     }
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/api/PropertyVersions.java b/versions-common/src/main/java/org/codehaus/mojo/versions/api/PropertyVersions.java
index 7578d3cb..dac48b27 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/api/PropertyVersions.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/api/PropertyVersions.java
@@ -19,12 +19,8 @@ package org.codehaus.mojo.versions.api;
  * under the License.
  */
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.SortedSet;
@@ -38,6 +34,7 @@ import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException
 import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
 import org.apache.maven.artifact.versioning.Restriction;
 import org.apache.maven.artifact.versioning.VersionRange;
+import org.apache.maven.plugin.logging.Log;
 import org.apache.maven.project.MavenProject;
 import org.codehaus.mojo.versions.ordering.BoundArtifactVersion;
 import org.codehaus.mojo.versions.ordering.InvalidSegmentException;
@@ -59,61 +56,33 @@ public class PropertyVersions extends AbstractVersionDetails {
 
     private final Set<ArtifactAssociation> associations;
 
-    private final VersionsHelper helper;
-
     /**
      * The available versions.
      *
      * @since 1.0-beta-1
      */
-    private final SortedSet<ArtifactVersion> versions;
+    private final SortedSet<ArtifactVersion> resolvedVersions;
+
+    private final Log log;
+
+    private VersionsHelper helper;
 
     private final PropertyVersions.PropertyVersionComparator comparator;
 
-    PropertyVersions(String profileId, String name, VersionsHelper helper, Set<ArtifactAssociation> associations)
-            throws VersionRetrievalException {
+    PropertyVersions(
+            String profileId,
+            String name,
+            Log log,
+            VersionsHelper helper,
+            Set<ArtifactAssociation> associations,
+            SortedSet<ArtifactVersion> resolvedVersions) {
         this.profileId = profileId;
         this.name = name;
+        this.log = log;
         this.helper = helper;
         this.associations = new TreeSet<>(associations);
         this.comparator = new PropertyVersionComparator();
-        this.versions = resolveAssociatedVersions(helper, associations, comparator);
-    }
-
-    private static SortedSet<ArtifactVersion> resolveAssociatedVersions(
-            VersionsHelper helper, Set<ArtifactAssociation> associations, VersionComparator versionComparator)
-            throws VersionRetrievalException {
-        SortedSet<ArtifactVersion> versions = null;
-        for (ArtifactAssociation association : associations) {
-            final ArtifactVersions associatedVersions =
-                    helper.lookupArtifactVersions(association.getArtifact(), association.isUsePluginRepositories());
-            if (versions != null) {
-                final ArtifactVersion[] artifactVersions = associatedVersions.getVersions(true);
-                // since ArtifactVersion does not override equals, we have to do this the hard way
-                // result.retainAll( Arrays.asList( artifactVersions ) );
-                Iterator<ArtifactVersion> j = versions.iterator();
-                while (j.hasNext()) {
-                    boolean contains = false;
-                    ArtifactVersion version = j.next();
-                    for (ArtifactVersion artifactVersion : artifactVersions) {
-                        if (version.compareTo(artifactVersion) == 0) {
-                            contains = true;
-                            break;
-                        }
-                    }
-                    if (!contains) {
-                        j.remove();
-                    }
-                }
-            } else {
-                versions = new TreeSet<>(versionComparator);
-                versions.addAll(Arrays.asList(associatedVersions.getVersions(true)));
-            }
-        }
-        if (versions == null) {
-            versions = new TreeSet<>(versionComparator);
-        }
-        return Collections.unmodifiableSortedSet(versions);
+        this.resolvedVersions = resolvedVersions;
     }
 
     /**
@@ -142,12 +111,12 @@ public class PropertyVersions extends AbstractVersionDetails {
      *
      * @param artifacts The {@link Collection} of {@link Artifact} instances .
      * @return The versions that can be resolved from the supplied Artifact instances or an empty array if no version
-     * can be resolved (i.e. the property is not associated with any of the supplied artifacts or the property
-     * is also associated to an artifact that has not been provided).
+     *         can be resolved (i.e. the property is not associated with any of the supplied artifacts or the property
+     *         is also associated to an artifact that has not been provided).
      * @since 1.0-alpha-3
      */
     public ArtifactVersion[] getVersions(Collection<Artifact> artifacts) {
-        List<ArtifactVersion> result = new ArrayList<>();
+        SortedSet<ArtifactVersion> result = new TreeSet<>();
         // go through all the associations
         // see if they are met from the collection
         // add the version if they are
@@ -165,34 +134,23 @@ public class PropertyVersions extends AbstractVersionDetails {
                 }
             }
         }
-        // we now have a list of all the versions that partially satisfy the association requirements
-        Iterator<ArtifactVersion> k = result.iterator();
-        versions:
-        while (k.hasNext()) {
-            ArtifactVersion candidate = k.next();
-            associations:
-            for (ArtifactAssociation association : associations) {
-                for (Artifact artifact : artifacts) {
-                    if (association.getArtifact().getGroupId().equals(artifact.getGroupId())
-                            && association.getArtifact().getArtifactId().equals(artifact.getArtifactId())) {
-                        try {
-                            if (candidate
-                                    .toString()
-                                    .equals(artifact.getSelectedVersion().toString())) {
-                                // this association can be met, try the next
-                                continue associations;
-                            }
-                        } catch (OverConstrainedVersionException e) {
-                            // ignore this one again
-                        }
+
+        // Filter versions that satisfy all associations
+        result.removeIf(candidate -> associations.stream()
+                .anyMatch(association -> artifacts.stream().noneMatch(artifact -> {
+                    try {
+                        return association.getArtifact().getGroupId().equals(artifact.getGroupId())
+                                && association.getArtifact().getArtifactId().equals(artifact.getArtifactId())
+                                && candidate
+                                        .toString()
+                                        .equals(artifact.getSelectedVersion().toString());
+                    } catch (OverConstrainedVersionException e) {
+                        // ignore this one as we cannot resolve a valid version
+                        return false;
                     }
-                }
-                // candidate is not valid as at least one association cannot be met
-                k.remove();
-                continue versions;
-            }
-        }
-        return asArtifactVersionArray(result);
+                })));
+
+        return result.toArray(new ArtifactVersion[0]);
     }
 
     /**
@@ -202,80 +160,60 @@ public class PropertyVersions extends AbstractVersionDetails {
      * @param includeSnapshots Whether to include snapshot versions in our search.
      * @return The (possibly empty) array of versions.
      */
-    public synchronized ArtifactVersion[] getVersions(boolean includeSnapshots) {
-        Set<ArtifactVersion> result;
-        if (includeSnapshots) {
-            result = versions;
-        } else {
-            result = new TreeSet<>(getVersionComparator());
-            for (ArtifactVersion candidate : versions) {
-                if (ArtifactUtils.isSnapshot(candidate.toString())) {
-                    continue;
-                }
-                result.add(candidate);
-            }
-        }
-        return asArtifactVersionArray(result);
-    }
-
-    private ArtifactVersion[] asArtifactVersionArray(Collection<ArtifactVersion> result) {
-        if (result == null || result.isEmpty()) {
-            return new ArtifactVersion[0];
-        } else {
-            final ArtifactVersion[] answer = result.toArray(new ArtifactVersion[0]);
-            VersionComparator[] rules = lookupComparators();
-            assert rules.length > 0;
-            Arrays.sort(answer, rules[0]);
-            if (rules.length == 1 || answer.length == 1) {
-                // only one rule...
-                return answer;
-            }
-            ArtifactVersion[] alt = answer.clone();
-            for (int j = 1; j < rules.length; j++) {
-                Arrays.sort(alt, rules[j]);
-                if (!Arrays.equals(alt, answer)) {
-                    throw new IllegalStateException("Property " + name + " is associated with multiple artifacts"
-                            + " and these artifacts use different version sorting rules and these rules are effectively"
-                            + " incompatible for the set of versions available to this property.\nFirst rule says: "
-                            + Arrays.asList(answer) + "\nSecond rule says: "
-                            + Arrays.asList(alt));
-                }
-            }
-            return answer;
-        }
+    public ArtifactVersion[] getVersions(boolean includeSnapshots) {
+        return resolvedVersions.stream()
+                .filter(v -> includeSnapshots || !ArtifactUtils.isSnapshot(v.toString()))
+                .toArray(ArtifactVersion[]::new);
     }
 
+    /**
+     * Returns the name of the property
+     * @return name of the property
+     */
     public String getName() {
         return name;
     }
 
+    /**
+     * Returns the id of the profile
+     * @return id of the profile
+     */
     public String getProfileId() {
         return profileId;
     }
 
+    /**
+     * Says whether the property is associated with a dependency
+     * @return {@code true} if the property is associated with a dependency
+     */
     public boolean isAssociated() {
         return !associations.isEmpty();
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public String toString() {
-        return "PropertyVersions{" + (profileId == null ? "" : "profileId='" + profileId + "', ") + "name='" + name
-                + '\'' + ", associations=" + associations + '}';
+        return "PropertyVersions{" + (profileId == null ? "" : "profileId='" + profileId + "', ")
+                + "name='" + name + '\''
+                + ", associations=" + associations + '}';
     }
 
     /**
      * Retrieves the newest artifact version for the given property-denoted artifact or {@code null} if no newer
      * version could be found.
      *
-     * @param versionString current version of the artifact
-     * @param property property name indicating the artifact
-     * @param allowSnapshots whether snapshots should be considered
-     * @param reactorProjects collection of reactor projects
-     * @param helper VersionHelper object
-     * @param allowDowngrade whether downgrades should be allowed
+     * @param versionString     current version of the artifact
+     * @param property          property name indicating the artifact
+     * @param allowSnapshots    whether snapshots should be considered
+     * @param reactorProjects   collection of reactor projects
+     * @param allowDowngrade    whether downgrades should be allowed
      * @param upperBoundSegment the upper bound segment; empty() means no upper bound
      * @return newest artifact version fulfilling the criteria or null if no newer version could be found
-     * @throws InvalidSegmentException thrown if the {@code unchangedSegment} is not valid (e.g. greater than the number
-     * of segments in the version string)
+     * @throws InvalidSegmentException              thrown if the {@code unchangedSegment} is not valid (e.g. greater
+     *                                              than the number
+     *                                              of segments in the version string)
      * @throws InvalidVersionSpecificationException thrown if the version string in the property is not valid
      */
     public ArtifactVersion getNewestVersion(
@@ -283,18 +221,17 @@ public class PropertyVersions extends AbstractVersionDetails {
             Property property,
             boolean allowSnapshots,
             Collection<MavenProject> reactorProjects,
-            VersionsHelper helper,
             boolean allowDowngrade,
             Optional<Segment> upperBoundSegment)
             throws InvalidSegmentException, InvalidVersionSpecificationException {
-        final boolean includeSnapshots = !property.isBanSnapshots() && allowSnapshots;
-        helper.getLog().debug("getNewestVersion(): includeSnapshots='" + includeSnapshots + "'");
-        helper.getLog()
-                .debug("Property ${" + property.getName() + "}: Set of valid available versions is "
-                        + Arrays.asList(getVersions(includeSnapshots)));
+        boolean includeSnapshots = !property.isBanSnapshots() && allowSnapshots;
+        log.debug("getNewestVersion(): includeSnapshots='" + includeSnapshots + "'");
+        log.debug("Property ${" + property.getName() + "}: Set of valid available versions is "
+                + Arrays.asList(getVersions(includeSnapshots)));
+
         VersionRange range =
                 property.getVersion() != null ? VersionRange.createFromVersionSpec(property.getVersion()) : null;
-        helper.getLog().debug("Property ${" + property.getName() + "}: Restricting results to " + range);
+        log.debug("Property ${" + property.getName() + "}: Restricting results to " + range);
 
         ArtifactVersion currentVersion = ArtifactVersionService.getArtifactVersion(versionString);
         ArtifactVersion lowerBound = allowDowngrade
@@ -302,65 +239,45 @@ public class PropertyVersions extends AbstractVersionDetails {
                         .map(ArtifactVersionService::getArtifactVersion)
                         .orElse(null)
                 : currentVersion;
-        if (helper.getLog().isDebugEnabled()) {
-            helper.getLog().debug("lowerBoundArtifactVersion: " + lowerBound);
-        }
+        log.debug("lowerBoundArtifactVersion: " + lowerBound);
 
-        ArtifactVersion upperBound = !upperBoundSegment.isPresent()
-                ? null
-                : upperBoundSegment
-                        .map(s -> (ArtifactVersion) new BoundArtifactVersion(
+        ArtifactVersion upperBound = upperBoundSegment.isPresent()
+                ? upperBoundSegment
+                        .map(s -> new BoundArtifactVersion(
                                 currentVersion, s.isMajorTo(SUBINCREMENTAL) ? Segment.minorTo(s) : s))
-                        .orElse(null);
-        if (helper.getLog().isDebugEnabled()) {
-            helper.getLog().debug("Property ${" + property.getName() + "}: upperBound is: " + upperBound);
-        }
+                        .orElse(null)
+                : null;
+        log.debug("Property ${" + property.getName() + "}: upperBound is: " + upperBound);
 
         Restriction restriction = new Restriction(lowerBound, allowDowngrade, upperBound, allowDowngrade);
         ArtifactVersion result = getNewestVersion(range, restriction, includeSnapshots);
 
-        helper.getLog().debug("Property ${" + property.getName() + "}: Current winner is: " + result);
+        log.debug("Property ${" + property.getName() + "}: Current winner is: " + result);
 
         if (property.isSearchReactor()) {
-            helper.getLog().debug("Property ${" + property.getName() + "}: Searching reactor for a valid version...");
+            log.debug("Property ${" + property.getName() + "}: Searching reactor for a valid version...");
             Set<Artifact> reactorArtifacts =
                     reactorProjects.stream().map(MavenProject::getArtifact).collect(Collectors.toSet());
             ArtifactVersion[] reactorVersions = getVersions(reactorArtifacts);
-            helper.getLog()
-                    .debug("Property ${" + property.getName()
-                            + "}: Set of valid available versions from the reactor is "
-                            + Arrays.asList(reactorVersions));
-            ArtifactVersion fromReactor = null;
-            if (reactorVersions.length > 0) {
-                for (int j = reactorVersions.length - 1; j >= 0; j--) {
-                    if (range == null || ArtifactVersions.isVersionInRange(reactorVersions[j], range)) {
-                        fromReactor = reactorVersions[j];
-                        helper.getLog()
-                                .debug("Property ${" + property.getName() + "}: Reactor has version " + fromReactor);
-                        break;
-                    }
-                }
-            }
-            if (fromReactor != null && (result != null || !currentVersion.equals(fromReactor.toString()))) {
-                if (property.isPreferReactor()) {
-                    helper.getLog()
-                            .debug("Property ${" + property.getName()
-                                    + "}: Reactor has a version and we prefer the reactor");
-                    result = fromReactor;
-                } else {
-                    if (result == null) {
-                        helper.getLog().debug("Property ${" + property.getName() + "}: Reactor has the only version");
-                        result = fromReactor;
-                    } else if (getVersionComparator().compare(result, fromReactor) < 0) {
-                        helper.getLog().debug("Property ${" + property.getName() + "}: Reactor has a newer version");
-                        result = fromReactor;
-                    } else {
-                        helper.getLog()
-                                .debug("Property ${" + property.getName() + "}: Reactor has the same or older version");
-                    }
-                }
+            log.debug("Property ${" + property.getName() + "}: Set of valid available versions from the reactor is "
+                    + Arrays.asList(reactorVersions));
+
+            ArtifactVersion fromReactor = Arrays.stream(reactorVersions)
+                    .filter(version -> range == null || ArtifactVersions.isVersionInRange(version, range))
+                    .reduce((first, second) -> second)
+                    .orElse(null);
+
+            if (fromReactor != null && (result == null || !currentVersion.equals(fromReactor.toString()))) {
+                result = property.isPreferReactor() || result == null
+                        ? fromReactor
+                        : result.compareTo(fromReactor) < 0 ? fromReactor : result;
+                log.debug("Property ${" + property.getName() + "}: "
+                        + (result.equals(fromReactor)
+                                ? "Reactor has a valid version"
+                                : "Reactor has the same or older version"));
             }
         }
+
         return result;
     }
 
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/api/PropertyVersionsBuilder.java b/versions-common/src/main/java/org/codehaus/mojo/versions/api/PropertyVersionsBuilder.java
index b21b1835..78bd645c 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/api/PropertyVersionsBuilder.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/api/PropertyVersionsBuilder.java
@@ -19,18 +19,24 @@ package org.codehaus.mojo.versions.api;
  * under the License.
  */
 
-import java.util.Comparator;
+import java.util.Arrays;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
+import java.util.SortedSet;
 import java.util.TreeSet;
 
+import org.apache.commons.lang3.tuple.Pair;
 import org.apache.maven.artifact.Artifact;
 import org.apache.maven.artifact.versioning.ArtifactVersion;
 import org.apache.maven.artifact.versioning.VersionRange;
-import org.codehaus.mojo.versions.ordering.VersionComparator;
+import org.apache.maven.plugin.logging.Log;
 import org.codehaus.mojo.versions.utils.ArtifactVersionService;
 
+import static java.util.Optional.ofNullable;
+
 /**
  * Builds {@link org.codehaus.mojo.versions.api.PropertyVersions} instances.
  *
@@ -44,12 +50,14 @@ public class PropertyVersionsBuilder {
 
     private final Set<ArtifactAssociation> associations;
 
-    private final VersionsHelper helper;
-
     private final Map<String, Boolean> upperBounds = new LinkedHashMap<>();
 
     private final Map<String, Boolean> lowerBounds = new LinkedHashMap<>();
 
+    private final Log log;
+
+    private final VersionsHelper helper;
+
     private ArtifactVersion currentVersion;
 
     private VersionRange currentVersionRange;
@@ -61,11 +69,12 @@ public class PropertyVersionsBuilder {
      * @param name The property name.
      * @param helper The {@link org.codehaus.mojo.versions.api.DefaultVersionsHelper}.
      */
-    PropertyVersionsBuilder(VersionsHelper helper, String profileId, String name) {
+    PropertyVersionsBuilder(VersionsHelper helper, String profileId, String name, Log log) {
+        this.helper = helper;
         this.profileId = profileId;
         this.name = name;
         this.associations = new TreeSet<>();
-        this.helper = helper;
+        this.log = log;
     }
 
     public PropertyVersionsBuilder withAssociation(Artifact artifact, boolean usePluginRepositories) {
@@ -81,12 +90,9 @@ public class PropertyVersionsBuilder {
         return !associations.isEmpty();
     }
 
-    public ArtifactAssociation[] getAssociations() {
-        return associations.toArray(new ArtifactAssociation[0]);
-    }
-
     public PropertyVersions build() throws VersionRetrievalException {
-        PropertyVersions instance = new PropertyVersions(profileId, name, helper, associations);
+        SortedSet<ArtifactVersion> resolvedVersions = resolveAssociatedVersions(helper, associations);
+        PropertyVersions instance = new PropertyVersions(profileId, name, log, helper, associations, resolvedVersions);
         instance.setCurrentVersion(currentVersion);
         instance.setCurrentVersionRange(currentVersionRange);
         return instance;
@@ -97,120 +103,37 @@ public class PropertyVersionsBuilder {
     }
 
     public String getVersionRange() {
-        Comparator<ArtifactVersion> comparator = new PropertyVersionComparator();
         if (lowerBounds.isEmpty() && upperBounds.isEmpty()) {
             return null;
         }
-        ArtifactVersion lowerBound = null;
-        boolean includeLower = true;
-        for (Map.Entry<String, Boolean> entry : lowerBounds.entrySet()) {
-            ArtifactVersion candidate = ArtifactVersionService.getArtifactVersion(entry.getKey());
-            if (lowerBound == null) {
-                lowerBound = candidate;
-                includeLower = entry.getValue();
-            } else {
-                final int result = comparator.compare(lowerBound, candidate);
-                if (result > 0) {
-                    lowerBound = candidate;
-                    includeLower = entry.getValue();
-                } else if (result == 0) {
-                    includeLower = includeLower && entry.getValue();
-                }
-            }
-        }
-        ArtifactVersion upperBound = null;
-        boolean includeUpper = true;
-        for (Map.Entry<String, Boolean> entry : upperBounds.entrySet()) {
-            ArtifactVersion candidate = ArtifactVersionService.getArtifactVersion(entry.getKey());
-            if (upperBound == null) {
-                upperBound = candidate;
-                includeUpper = entry.getValue();
-            } else {
-                final int result = comparator.compare(upperBound, candidate);
-                if (result < 0) {
-                    upperBound = candidate;
-                    includeUpper = entry.getValue();
-                } else if (result == 0) {
-                    includeUpper = includeUpper && entry.getValue();
-                }
-            }
-        }
-        StringBuilder buf = new StringBuilder();
-        if (includeLower) {
-            buf.append('[');
-        } else {
-            buf.append('(');
-        }
-        if (lowerBound != null) {
-            buf.append(lowerBound);
-        }
-        buf.append(',');
-        if (upperBound != null) {
-            buf.append(upperBound);
-        }
-        if (includeUpper) {
-            buf.append(']');
-        } else {
-            buf.append(')');
-        }
-        return buf.toString();
+
+        Optional<Pair<ArtifactVersion, Boolean>> lowerBound = lowerBounds.entrySet().stream()
+                .min(Map.Entry.comparingByKey())
+                .map(e -> Pair.of(e.getKey(), e.getValue()))
+                .map(p -> Pair.of(ArtifactVersionService.getArtifactVersion(p.getKey()), p.getValue()));
+
+        Optional<Pair<ArtifactVersion, Boolean>> upperBound = upperBounds.entrySet().stream()
+                .max(Map.Entry.comparingByKey())
+                .map(e -> Pair.of(e.getKey(), e.getValue()))
+                .map(p -> Pair.of(ArtifactVersionService.getArtifactVersion(p.getKey()), p.getValue()));
+
+        return (lowerBound.map(Pair::getValue).orElse(false) ? '[' : '(')
+                + lowerBound.map(Pair::getKey).map(Object::toString).orElse("")
+                + ','
+                + upperBound.map(Pair::getKey).map(Object::toString).orElse("")
+                + (upperBound.map(Pair::getValue).orElse(false) ? ']' : ')');
     }
 
     public PropertyVersionsBuilder withLowerBound(String lowerBound, boolean includeLower) {
-        Boolean value = lowerBounds.get(lowerBound);
-        if (value == null) {
-            value = includeLower;
-        } else {
-            value = includeLower && value;
-        }
-        lowerBounds.put(lowerBound, value);
+        lowerBounds.compute(lowerBound, (__, oldValue) -> ofNullable(oldValue).orElse(true) && includeLower);
         return this;
     }
 
     public PropertyVersionsBuilder withUpperBound(String upperBound, boolean includeUpper) {
-        Boolean value = upperBounds.get(upperBound);
-        if (value == null) {
-            value = includeUpper;
-        } else {
-            value = includeUpper && value;
-        }
-        upperBounds.put(upperBound, value);
+        upperBounds.compute(upperBound, (__, oldValue) -> ofNullable(oldValue).orElse(true) && includeUpper);
         return this;
     }
 
-    private VersionComparator[] lookupComparators() {
-        return associations.stream()
-                .map(association -> helper.getVersionComparator(association.getArtifact()))
-                .distinct()
-                .toArray(VersionComparator[]::new);
-    }
-
-    private final class PropertyVersionComparator implements Comparator<ArtifactVersion> {
-        public int compare(ArtifactVersion v1, ArtifactVersion v2) {
-            return innerCompare(v1, v2);
-        }
-
-        private int innerCompare(ArtifactVersion v1, ArtifactVersion v2) {
-            if (!isAssociated()) {
-                throw new IllegalStateException("Cannot compare versions for a property with no associations");
-            }
-            VersionComparator[] comparators = lookupComparators();
-            assert comparators.length >= 1 : "we have at least one association => at least one comparator";
-            int result = comparators[0].compare(v1, v2);
-            for (int i = 1; i < comparators.length; i++) {
-                int alt = comparators[i].compare(v1, v2);
-                if (result != alt && (result >= 0 && alt < 0) || (result <= 0 && alt > 0)) {
-                    throw new IllegalStateException("Property " + name + " is associated with multiple artifacts"
-                            + " and these artifacts use different version sorting rules and these rules are effectively"
-                            + " incompatible for the two of versions being compared.\nFirst rule says compare(\"" + v1
-                            + "\", \"" + v2 + "\") = " + result + "\nSecond rule says compare(\"" + v1 + "\", \"" + v2
-                            + "\") = " + alt);
-                }
-            }
-            return result;
-        }
-    }
-
     public PropertyVersionsBuilder withCurrentVersion(ArtifactVersion currentVersion) {
         this.currentVersion = currentVersion;
         return this;
@@ -220,4 +143,20 @@ public class PropertyVersionsBuilder {
         this.currentVersionRange = currentVersionRange;
         return this;
     }
+
+    private SortedSet<ArtifactVersion> resolveAssociatedVersions(
+            VersionsHelper helper, Set<ArtifactAssociation> associations) throws VersionRetrievalException {
+        SortedSet<ArtifactVersion> result = new TreeSet<>();
+        for (ArtifactAssociation association : associations) {
+            ArtifactVersions artifactVersions =
+                    helper.lookupArtifactVersions(association.getArtifact(), association.isUsePluginRepositories());
+            List<ArtifactVersion> associatedVersions = Arrays.asList(artifactVersions.getVersions(true));
+            if (result.isEmpty()) {
+                result.addAll(associatedVersions);
+            } else {
+                result.retainAll(associatedVersions);
+            }
+        }
+        return result;
+    }
 }
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/api/VersionsHelper.java b/versions-common/src/main/java/org/codehaus/mojo/versions/api/VersionsHelper.java
index fd4453b7..ec3555a8 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/api/VersionsHelper.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/api/VersionsHelper.java
@@ -28,7 +28,6 @@ import org.apache.maven.artifact.versioning.VersionRange;
 import org.apache.maven.model.Dependency;
 import org.apache.maven.model.Plugin;
 import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugin.logging.Log;
 import org.apache.maven.project.MavenProject;
 import org.codehaus.mojo.versions.ordering.VersionComparator;
 import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
@@ -40,12 +39,6 @@ import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator
  * @since 1.0-alpha-3
  */
 public interface VersionsHelper {
-    /**
-     * Gets the logger used by this helper.
-     *
-     * @return the logger used by this helper.
-     */
-    Log getLog();
 
     /**
      * Returns the version comparator to use for the specified artifact.
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/change/AbstractVersionChanger.java b/versions-common/src/main/java/org/codehaus/mojo/versions/change/AbstractVersionChanger.java
index 30b995e0..39a5ec25 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/change/AbstractVersionChanger.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/change/AbstractVersionChanger.java
@@ -30,27 +30,54 @@ import org.codehaus.mojo.versions.rewriting.MutableXMLStreamReader;
  * @since 15-Sep-2010 15:59:46
  */
 public abstract class AbstractVersionChanger implements VersionChanger {
+    /**
+     * {@link Model} instance
+     */
     private final Model model;
 
+    /**
+     * {@link MutableXMLStreamReader} instance
+     */
     private final MutableXMLStreamReader pom;
 
-    protected final Log log;
+    /**
+     * {@link Log} instance
+     */
+    private final Log log;
 
+    /**
+     * Creates a new instance providing the model, mutable pom, and a logger.
+     * @param model {@link Model} instance
+     * @param pom {@link MutableXMLStreamReader} instance representing the parsed (mutable) pom file
+     * @param log {@link Log} instance for logging
+     */
     public AbstractVersionChanger(Model model, MutableXMLStreamReader pom, Log log) {
         this.model = model;
         this.pom = pom;
         this.log = log;
     }
 
-    public Model getModel() {
+    /**
+     * Returns the Maven Model instance
+     * @return {@link Model} instance
+     */
+    protected Model getModel() {
         return model;
     }
 
-    public MutableXMLStreamReader getPom() {
+    /**
+     * Returns the {@link MutableXMLStreamReader} instance representing the parsed and possibly modified POM file.
+     * @return {@link MutableXMLStreamReader} instance under changes
+     */
+    protected MutableXMLStreamReader getPom() {
         return pom;
     }
 
-    public void info(CharSequence charSequence) {
-        log.info(charSequence);
+    /**
+     * Returns the {@link Log} instance
+     * @return {@link Log} instance
+     */
+    protected Log getLog() {
+        return log;
     }
 }
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/change/CompositeVersionChanger.java b/versions-common/src/main/java/org/codehaus/mojo/versions/change/CompositeVersionChanger.java
index df3da76a..64a391a8 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/change/CompositeVersionChanger.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/change/CompositeVersionChanger.java
@@ -24,22 +24,32 @@ import java.util.List;
 import org.codehaus.mojo.versions.api.change.DependencyVersionChange;
 
 /**
- * Created by IntelliJ IDEA.
- *
- * @author Stephen Connolly
- * @since 15-Sep-2010 16:01:35
+ * A composite version changer, which chains version changers together and executes the chain
+ * in {@link #apply(DependencyVersionChange)} until an exception occurs or the chain is through.
  */
 public class CompositeVersionChanger implements VersionChanger {
     private final List<VersionChanger> composites;
 
+    /**
+     * Construct a new composite version changed based on an array of version changers
+     * @param composites array of version changes to compose from
+     */
     public CompositeVersionChanger(VersionChanger[] composites) {
         this.composites = new ArrayList<>(Arrays.asList(composites));
     }
 
+    /**
+     * Construct a new composite version changed based on a {@link List} of version changers
+     * @param composites list of version changes to compose from
+     */
     public CompositeVersionChanger(List<VersionChanger> composites) {
         this.composites = new ArrayList<>(composites);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public void apply(DependencyVersionChange versionChange) throws XMLStreamException {
         for (VersionChanger delegate : composites) {
             delegate.apply(versionChange);
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/change/DependencyVersionChanger.java b/versions-common/src/main/java/org/codehaus/mojo/versions/change/DependencyVersionChanger.java
index 6a7062ad..54479b29 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/change/DependencyVersionChanger.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/change/DependencyVersionChanger.java
@@ -31,10 +31,19 @@ import org.codehaus.mojo.versions.rewriting.MutableXMLStreamReader;
  */
 public class DependencyVersionChanger extends AbstractVersionChanger {
 
+    /**
+     * Constructs a new instance
+     * @param model {@link Model} instance
+     * @param pom {@link MutableXMLStreamReader} representing the pom file to be modified
+     * @param reporter {@link Log} object
+     */
     public DependencyVersionChanger(Model model, MutableXMLStreamReader pom, Log reporter) {
         super(model, pom, reporter);
     }
-
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public void apply(DependencyVersionChange versionChange) throws XMLStreamException {
         if (PomHelper.setDependencyVersion(
                 getPom(),
@@ -43,9 +52,11 @@ public class DependencyVersionChanger extends AbstractVersionChanger {
                 versionChange.getOldVersion(),
                 versionChange.getNewVersion(),
                 getModel(),
-                log)) {
-            info("    Updating dependency " + versionChange.getGroupId() + ":" + versionChange.getArtifactId());
-            info("        from version " + versionChange.getOldVersion() + " to " + versionChange.getNewVersion());
+                getLog())) {
+            getLog().info("    Updating dependency " + versionChange.getGroupId() + ":"
+                    + versionChange.getArtifactId());
+            getLog().info("        from version " + versionChange.getOldVersion() + " to "
+                    + versionChange.getNewVersion());
         }
     }
 }
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/change/ParentVersionChanger.java b/versions-common/src/main/java/org/codehaus/mojo/versions/change/ParentVersionChanger.java
index 08f1c1b0..ba8ee488 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/change/ParentVersionChanger.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/change/ParentVersionChanger.java
@@ -24,21 +24,33 @@ import org.codehaus.mojo.versions.api.change.DependencyVersionChange;
 import org.codehaus.mojo.versions.rewriting.MutableXMLStreamReader;
 
 /**
- *
+ * Version changer for the parent
  */
 public class ParentVersionChanger extends AbstractVersionChanger {
 
+    /**
+     * Constructs a new instance
+     * @param model {@link Model} instance
+     * @param pom {@link MutableXMLStreamReader} representing the pom file to be modified
+     * @param reporter {@link Log} object
+     */
     public ParentVersionChanger(Model model, MutableXMLStreamReader pom, Log reporter) {
         super(model, pom, reporter);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public void apply(DependencyVersionChange versionChange) throws XMLStreamException {
         if (getModel().getParent() != null
                 && versionChange.getGroupId().equals(getModel().getParent().getGroupId())
                 && versionChange.getArtifactId().equals(getModel().getParent().getArtifactId())) {
             if (PomHelper.setProjectParentVersion(getPom(), versionChange.getNewVersion())) {
-                info("    Updating parent " + versionChange.getGroupId() + ":" + versionChange.getArtifactId());
-                info("        from version " + versionChange.getOldVersion() + " to " + versionChange.getNewVersion());
+                getLog().info("    Updating parent " + versionChange.getGroupId() + ":"
+                        + versionChange.getArtifactId());
+                getLog().info("        from version " + versionChange.getOldVersion() + " to "
+                        + versionChange.getNewVersion());
             }
         }
     }
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/change/PluginVersionChanger.java b/versions-common/src/main/java/org/codehaus/mojo/versions/change/PluginVersionChanger.java
index feed1486..6c57f969 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/change/PluginVersionChanger.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/change/PluginVersionChanger.java
@@ -31,10 +31,20 @@ import org.codehaus.mojo.versions.rewriting.MutableXMLStreamReader;
  */
 public class PluginVersionChanger extends AbstractVersionChanger {
 
+    /**
+     * Constructs a new instance
+     * @param model {@link Model} instance
+     * @param pom {@link MutableXMLStreamReader} representing the pom file to be modified
+     * @param reporter {@link Log} object
+     */
     public PluginVersionChanger(Model model, MutableXMLStreamReader pom, Log reporter) {
         super(model, pom, reporter);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public void apply(DependencyVersionChange versionChange) throws XMLStreamException {
         if (PomHelper.setPluginVersion(
                 getPom(),
@@ -42,8 +52,9 @@ public class PluginVersionChanger extends AbstractVersionChanger {
                 versionChange.getArtifactId(),
                 versionChange.getOldVersion(),
                 versionChange.getNewVersion())) {
-            info("    Updating plugin " + versionChange.getGroupId() + ":" + versionChange.getArtifactId());
-            info("        from version " + versionChange.getOldVersion() + " to " + versionChange.getNewVersion());
+            getLog().info("    Updating plugin " + versionChange.getGroupId() + ":" + versionChange.getArtifactId());
+            getLog().info("        from version " + versionChange.getOldVersion() + " to "
+                    + versionChange.getNewVersion());
         }
     }
 }
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/change/ProjectVersionChanger.java b/versions-common/src/main/java/org/codehaus/mojo/versions/change/ProjectVersionChanger.java
index 20f4e258..03a76eab 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/change/ProjectVersionChanger.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/change/ProjectVersionChanger.java
@@ -31,16 +31,28 @@ import org.codehaus.mojo.versions.rewriting.MutableXMLStreamReader;
  */
 public class ProjectVersionChanger extends AbstractVersionChanger {
 
+    /**
+     * Constructs a new instance
+     * @param model {@link Model} instance
+     * @param pom {@link MutableXMLStreamReader} representing the pom file to be modified
+     * @param reporter {@link Log} object
+     */
     public ProjectVersionChanger(Model model, MutableXMLStreamReader pom, Log reporter) {
         super(model, pom, reporter);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public void apply(DependencyVersionChange versionChange) throws XMLStreamException {
         if (versionChange.getGroupId().equals(PomHelper.getGroupId(getModel()))
                 && versionChange.getArtifactId().equals(PomHelper.getArtifactId(getModel()))) {
             if (PomHelper.setProjectVersion(getPom(), versionChange.getNewVersion())) {
-                info("    Updating project " + versionChange.getGroupId() + ":" + versionChange.getArtifactId());
-                info("        from version " + versionChange.getOldVersion() + " to " + versionChange.getNewVersion());
+                getLog().info("    Updating project " + versionChange.getGroupId() + ":"
+                        + versionChange.getArtifactId());
+                getLog().info("        from version " + versionChange.getOldVersion() + " to "
+                        + versionChange.getNewVersion());
             }
         }
     }
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/change/VersionChanger.java b/versions-common/src/main/java/org/codehaus/mojo/versions/change/VersionChanger.java
index 5a5b87f1..75c7799b 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/change/VersionChanger.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/change/VersionChanger.java
@@ -20,11 +20,14 @@ import javax.xml.stream.XMLStreamException;
 import org.codehaus.mojo.versions.api.change.DependencyVersionChange;
 
 /**
- * Created by IntelliJ IDEA.
- *
- * @author Stephen Connolly
- * @since 15-Sep-2010 15:59:00
+ * An abstract version changer, capable of changing versions of a given dependency.
  */
 public interface VersionChanger {
+    /**
+     * Applies the implemented change on the given dependency.
+     *
+     * @param versionChange dependency to be changed
+     * @throws XMLStreamException thrown if the underlying XML operation does not succeed
+     */
     void apply(DependencyVersionChange versionChange) throws XMLStreamException;
 }
diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/recording/DefaultDependencyChangeRecord.java b/versions-common/src/main/java/org/codehaus/mojo/versions/recording/DefaultDependencyChangeRecord.java
index 330982c5..e3f74ac4 100644
--- a/versions-common/src/main/java/org/codehaus/mojo/versions/recording/DefaultDependencyChangeRecord.java
+++ b/versions-common/src/main/java/org/codehaus/mojo/versions/recording/DefaultDependencyChangeRecord.java
@@ -46,39 +46,71 @@ public class DefaultDependencyChangeRecord implements DependencyChangeRecord {
         return new Builder();
     }
 
+    /**
+     * A builder object for {@link DependencyChangeRecord} instances
+     */
     public static class Builder {
-
         private ChangeKind kind;
         private String groupId;
         private String artifactId;
         private String oldVersion;
         private String newVersion;
 
+        /**
+         * Supplies the kind
+         * @param kind requested kind
+         * @return builder instance
+         */
         public Builder withKind(ChangeKind kind) {
             this.kind = kind;
             return this;
         }
 
+        /**
+         * Supplies the groupId
+         * @param groupId requested groupId
+         * @return builder instance
+         */
         public Builder withGroupId(String groupId) {
             this.groupId = groupId;
             return this;
         }
 
+        /**
+         * Supplies the artifactId
+         * @param artifactId requested artifactId
+         * @return builder instance
+         */
         public Builder withArtifactId(String artifactId) {
             this.artifactId = artifactId;
             return this;
         }
 
+        /**
+         * Supplies the version from before the change
+         * @param oldVersion version from before the change
+         * @return builder instance
+         */
         public Builder withOldVersion(String oldVersion) {
             this.oldVersion = oldVersion;
             return this;
         }
 
+        /**
+         * Supplies the version from after the change
+         * @param newVersion version from after the change
+         * @return builder instance
+         */
         public Builder withNewVersion(String newVersion) {
             this.newVersion = newVersion;
             return this;
         }
 
+        /**
+         * Supplies the dependency
+         * @param dependency requested dependency
+         * @return builder instance
+         */
         public Builder withDependency(Dependency dependency) {
             groupId = dependency.getGroupId();
             artifactId = dependency.getArtifactId();
@@ -86,6 +118,11 @@ public class DefaultDependencyChangeRecord implements DependencyChangeRecord {
             return this;
         }
 
+        /**
+         * Supplies the artifact
+         * @param artifact requested artifact
+         * @return builder instance
+         */
         public Builder withArtifact(Artifact artifact) {
             groupId = artifact.getGroupId();
             artifactId = artifact.getArtifactId();
@@ -93,6 +130,10 @@ public class DefaultDependencyChangeRecord implements DependencyChangeRecord {
             return this;
         }
 
+        /**
+         * Builds the {@link DependencyChangeRecord} instance
+         * @return {@link DependencyChangeRecord} instance
+         */
         public DependencyChangeRecord build() {
             return new DefaultDependencyChangeRecord(
                     kind, new DefaultDependencyVersionChange(groupId, artifactId, oldVersion, newVersion));
diff --git a/versions-enforcer/src/main/java/org/codehaus/mojo/versions/enforcer/MaxDependencyUpdates.java b/versions-enforcer/src/main/java/org/codehaus/mojo/versions/enforcer/MaxDependencyUpdates.java
index bef9330f..a3266791 100644
--- a/versions-enforcer/src/main/java/org/codehaus/mojo/versions/enforcer/MaxDependencyUpdates.java
+++ b/versions-enforcer/src/main/java/org/codehaus/mojo/versions/enforcer/MaxDependencyUpdates.java
@@ -42,12 +42,14 @@ import org.apache.maven.project.MavenProject;
 import org.apache.maven.wagon.Wagon;
 import org.codehaus.mojo.versions.api.ArtifactVersions;
 import org.codehaus.mojo.versions.api.DefaultVersionsHelper;
+import org.codehaus.mojo.versions.api.PomHelper;
 import org.codehaus.mojo.versions.api.Segment;
 import org.codehaus.mojo.versions.api.VersionRetrievalException;
 import org.codehaus.mojo.versions.api.VersionsHelper;
 import org.codehaus.mojo.versions.model.RuleSet;
 import org.codehaus.mojo.versions.utils.ArtifactFactory;
 import org.codehaus.mojo.versions.utils.DependencyComparator;
+import org.codehaus.mojo.versions.utils.VersionsExpressionEvaluator;
 import org.eclipse.aether.RepositorySystem;
 
 import static java.util.Collections.emptyList;
@@ -295,6 +297,8 @@ public class MaxDependencyUpdates extends AbstractEnforcerRule {
     private VersionsHelper createVersionsHelper(String serverId, String rulesUri, RuleSet ruleSet)
             throws EnforcerRuleError {
         try {
+            PomHelper pomHelper =
+                    new PomHelper(artifactFactory, new VersionsExpressionEvaluator(mavenSession, mojoExecution));
             return new DefaultVersionsHelper.Builder()
                     .withArtifactFactory(artifactFactory)
                     .withRepositorySystem(repositorySystem)
@@ -305,6 +309,7 @@ public class MaxDependencyUpdates extends AbstractEnforcerRule {
                     .withIgnoredVersions(null)
                     .withLog(new PluginLogWrapper(getLog()))
                     .withMavenSession(mavenSession)
+                    .withPomHelper(pomHelper)
                     .withMojoExecution(mojoExecution)
                     .build();
         } catch (MojoExecutionException e) {
diff --git a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/AbstractVersionsReport.java b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/AbstractVersionsReport.java
index 59767e6e..97b98c62 100644
--- a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/AbstractVersionsReport.java
+++ b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/AbstractVersionsReport.java
@@ -31,10 +31,12 @@ import org.apache.maven.reporting.AbstractMavenReport;
 import org.apache.maven.reporting.MavenReportException;
 import org.apache.maven.wagon.Wagon;
 import org.codehaus.mojo.versions.api.DefaultVersionsHelper;
+import org.codehaus.mojo.versions.api.PomHelper;
 import org.codehaus.mojo.versions.api.VersionsHelper;
 import org.codehaus.mojo.versions.model.RuleSet;
 import org.codehaus.mojo.versions.reporting.ReportRendererFactory;
 import org.codehaus.mojo.versions.utils.ArtifactFactory;
+import org.codehaus.mojo.versions.utils.VersionsExpressionEvaluator;
 import org.codehaus.plexus.i18n.I18N;
 import org.eclipse.aether.RepositorySystem;
 
@@ -176,6 +178,8 @@ public abstract class AbstractVersionsReport<T> extends AbstractMavenReport {
     public VersionsHelper getHelper() throws MavenReportException {
         if (helper == null) {
             try {
+                PomHelper pomHelper =
+                        new PomHelper(artifactFactory, new VersionsExpressionEvaluator(session, mojoExecution));
                 helper = new DefaultVersionsHelper.Builder()
                         .withArtifactFactory(artifactFactory)
                         .withRepositorySystem(repositorySystem)
@@ -186,6 +190,7 @@ public abstract class AbstractVersionsReport<T> extends AbstractMavenReport {
                         .withIgnoredVersions(ignoredVersions)
                         .withLog(getLog())
                         .withMavenSession(session)
+                        .withPomHelper(pomHelper)
                         .withMojoExecution(mojoExecution)
                         .build();
             } catch (MojoExecutionException e) {
@@ -244,10 +249,6 @@ public abstract class AbstractVersionsReport<T> extends AbstractMavenReport {
         return this.allowSnapshots;
     }
 
-    public String getComparisonMethod() {
-        return comparisonMethod;
-    }
-
     public I18N getI18n() {
         return i18n;
     }
diff --git a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/AbstractVersionsUpdaterMojo.java b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/AbstractVersionsUpdaterMojo.java
index e5497882..8f9911eb 100644
--- a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/AbstractVersionsUpdaterMojo.java
+++ b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/AbstractVersionsUpdaterMojo.java
@@ -59,6 +59,7 @@ import org.codehaus.mojo.versions.model.RuleSet;
 import org.codehaus.mojo.versions.ordering.InvalidSegmentException;
 import org.codehaus.mojo.versions.rewriting.MutableXMLStreamReader;
 import org.codehaus.mojo.versions.utils.ArtifactFactory;
+import org.codehaus.mojo.versions.utils.VersionsExpressionEvaluator;
 import org.eclipse.aether.RepositorySystem;
 
 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
@@ -206,6 +207,8 @@ public abstract class AbstractVersionsUpdaterMojo extends AbstractMojo {
 
     public VersionsHelper getHelper() throws MojoExecutionException {
         if (helper == null) {
+            PomHelper pomHelper =
+                    new PomHelper(artifactFactory, new VersionsExpressionEvaluator(session, mojoExecution));
             helper = new DefaultVersionsHelper.Builder()
                     .withArtifactFactory(artifactFactory)
                     .withRepositorySystem(repositorySystem)
@@ -216,6 +219,7 @@ public abstract class AbstractVersionsUpdaterMojo extends AbstractMojo {
                     .withIgnoredVersions(ignoredVersions)
                     .withLog(getLog())
                     .withMavenSession(session)
+                    .withPomHelper(pomHelper)
                     .withMojoExecution(mojoExecution)
                     .build();
         }
@@ -424,13 +428,7 @@ public abstract class AbstractVersionsUpdaterMojo extends AbstractMojo {
             throws XMLStreamException, InvalidVersionSpecificationException, InvalidSegmentException,
                     MojoExecutionException {
         ArtifactVersion winner = version.getNewestVersion(
-                currentVersion,
-                property,
-                getAllowSnapshots(),
-                this.reactorProjects,
-                this.getHelper(),
-                allowDowngrade,
-                unchangedSegment);
+                currentVersion, property, getAllowSnapshots(), this.reactorProjects, allowDowngrade, unchangedSegment);
 
         if (winner == null || currentVersion.equals(winner.toString())) {
             getLog().info("Property ${" + property.getName() + "}: Leaving unchanged as " + currentVersion);
diff --git a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/DisplayPropertyUpdatesMojo.java b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/DisplayPropertyUpdatesMojo.java
index 5c8d5b1d..79ba06e1 100644
--- a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/DisplayPropertyUpdatesMojo.java
+++ b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/DisplayPropertyUpdatesMojo.java
@@ -212,13 +212,7 @@ public class DisplayPropertyUpdatesMojo extends AbstractVersionsDisplayMojo {
             Optional<Segment> unchangedSegment = unchangedSegment1;
             try {
                 ArtifactVersion winner = version.getNewestVersion(
-                        currentVersion,
-                        property,
-                        this.allowSnapshots,
-                        this.reactorProjects,
-                        this.getHelper(),
-                        false,
-                        unchangedSegment);
+                        currentVersion, property, this.allowSnapshots, this.reactorProjects, false, unchangedSegment);
                 if (winner != null && !currentVersion.equals(winner.toString())) {
                     StringBuilder buf = new StringBuilder();
                     buf.append("${");


Test patch:
diff --git a/versions-common/src/test/java/org/codehaus/mojo/versions/api/DefaultVersionsHelperTest.java b/versions-common/src/test/java/org/codehaus/mojo/versions/api/DefaultVersionsHelperTest.java
index d60ead26..87ea4ee7 100644
--- a/versions-common/src/test/java/org/codehaus/mojo/versions/api/DefaultVersionsHelperTest.java
+++ b/versions-common/src/test/java/org/codehaus/mojo/versions/api/DefaultVersionsHelperTest.java
@@ -96,6 +96,12 @@ class DefaultVersionsHelperTest {
     @Mock
     ArtifactFactory artifactFactory;
 
+    @Mock
+    PomHelper pomHelper;
+
+    @Mock
+    Log log;
+
     @BeforeEach
     public void beforeEach() {
         MockitoAnnotations.openMocks(this);
@@ -274,11 +280,12 @@ class DefaultVersionsHelperTest {
 
         return new DefaultVersionsHelper.Builder()
                 .withArtifactFactory(artifactFactory)
+                .withPomHelper(pomHelper)
                 .withRepositorySystem(repositorySystem)
+                .withLog(log)
                 .withWagonMap(singletonMap("file", mockFileWagon(new URI(rulesUri))))
                 .withServerId("")
                 .withRulesUri(rulesUri)
-                .withLog(mock(Log.class))
                 .withMavenSession(mavenSession)
                 .withMojoExecution(mock(MojoExecution.class))
                 .build();
@@ -288,6 +295,7 @@ class DefaultVersionsHelperTest {
     void testIgnoredVersionsShouldBeTheOnlyPresentInAnEmptyRuleSet() throws MojoExecutionException {
         DefaultVersionsHelper versionsHelper = new DefaultVersionsHelper.Builder()
                 .withArtifactFactory(artifactFactory)
+                .withPomHelper(pomHelper)
                 .withLog(new SystemStreamLog())
                 .withIgnoredVersions(Arrays.asList(".*-M.", ".*-SNAPSHOT"))
                 .build();
@@ -304,6 +312,7 @@ class DefaultVersionsHelperTest {
     void testDefaultsShouldBePresentInAnEmptyRuleSet() throws MojoExecutionException, IllegalAccessException {
         DefaultVersionsHelper versionsHelper = new DefaultVersionsHelper.Builder()
                 .withArtifactFactory(artifactFactory)
+                .withPomHelper(pomHelper)
                 .withLog(new SystemStreamLog())
                 .withIgnoredVersions(singletonList(".*-M."))
                 .build();
@@ -315,6 +324,7 @@ class DefaultVersionsHelperTest {
     void testIgnoredVersionsShouldExtendTheRuleSet() throws MojoExecutionException, IllegalAccessException {
         DefaultVersionsHelper versionsHelper = new DefaultVersionsHelper.Builder()
                 .withArtifactFactory(artifactFactory)
+                .withPomHelper(pomHelper)
                 .withLog(new SystemStreamLog())
                 .withRuleSet(new RuleSet() {
                     {
diff --git a/versions-common/src/test/java/org/codehaus/mojo/versions/api/PomHelperTest.java b/versions-common/src/test/java/org/codehaus/mojo/versions/api/PomHelperTest.java
index 9afce7c2..7fd5ca66 100644
--- a/versions-common/src/test/java/org/codehaus/mojo/versions/api/PomHelperTest.java
+++ b/versions-common/src/test/java/org/codehaus/mojo/versions/api/PomHelperTest.java
@@ -34,6 +34,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.commons.io.FileUtils;
 import org.apache.maven.model.Model;
@@ -45,14 +46,26 @@ import org.apache.maven.plugin.logging.SystemStreamLog;
 import org.apache.maven.project.MavenProject;
 import org.codehaus.mojo.versions.rewriting.MutableXMLStreamReader;
 import org.codehaus.mojo.versions.utils.ModelNode;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
 
 import static java.nio.charset.Charset.defaultCharset;
 import static org.apache.commons.io.IOUtils.toInputStream;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasProperty;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.core.Is.is;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -64,11 +77,15 @@ import static org.mockito.Mockito.mock;
 /**
  * Tests the methods of {@link PomHelper}.
  */
+@ExtendWith(MockitoExtension.class)
 class PomHelperTest {
     private static final int NUMBER_OF_CHILD_PROJECTS = 30;
 
     private static final Path PATH = Paths.get("dummy-file");
 
+    @Mock
+    private Log log;
+
     /**
      * Tests what happens when changing a long property substitution pattern, e.g.
      * <a href="http://jira.codehaus.org/browse/MVERSIONS-44">MVERSIONS-44</a>
@@ -91,7 +108,166 @@ class PomHelperTest {
 
         assertEquals(newVersion, PomHelper.getProjectVersion(pom));
 
-        assertNotSame(oldVersion, newVersion);
+        assertNotSame(newVersion, oldVersion);
+    }
+
+    @Test
+    void testSetPropertyVersionNoProfile() throws Exception {
+        URL url = getClass().getResource("PomHelperTest.profiles.pom.xml");
+        assert url != null;
+        try (MutableXMLStreamReader pom = new MutableXMLStreamReader(Paths.get(url.toURI()))) {
+            assertThat(PomHelper.setPropertyVersion(pom, null, "propertyA", "newValue"), is(true));
+            String pomString = pom.getSource();
+            assertThat(pomString, containsString("<propertyA>newValue</propertyA>"));
+            assertThat(
+                    pomString,
+                    allOf(
+                            containsString("<propertyB>propertyBValue</propertyB>"),
+                            containsString("<profileAPropertyA>profileAPropertyAValue</profileAPropertyA>"),
+                            containsString("<profileAPropertyB>profileAPropertyBValue</profileAPropertyB>"),
+                            containsString("<profileBPropertyA>profileBPropertyAValue</profileBPropertyA>"),
+                            containsString("<profileBPropertyB>profileBPropertyBValue</profileBPropertyB>")));
+        }
+    }
+
+    @Test
+    void testSetPropertyVersionProfile() throws Exception {
+        URL url = getClass().getResource("PomHelperTest.profiles.pom.xml");
+        assert url != null;
+        try (MutableXMLStreamReader pom = new MutableXMLStreamReader(Paths.get(url.toURI()))) {
+            assertThat(PomHelper.setPropertyVersion(pom, "profileA", "profileAPropertyA", "newValue"), is(true));
+            String pomString = pom.getSource();
+            assertThat(pomString, containsString("<profileAPropertyA>newValue</profileAPropertyA>"));
+            assertThat(
+                    pomString,
+                    allOf(
+                            containsString("<propertyA>propertyAValue</propertyA>"),
+                            containsString("<propertyB>propertyBValue</propertyB>"),
+                            containsString("<profileAPropertyB>profileAPropertyBValue</profileAPropertyB>"),
+                            containsString("<profileBPropertyA>profileBPropertyAValue</profileBPropertyA>"),
+                            containsString("<profileBPropertyB>profileBPropertyBValue</profileBPropertyB>")));
+        }
+    }
+
+    @Test
+    void testImplicitProperties() throws Exception {
+        URL url = getClass().getResource("PomHelperTest.implicitProperties.pom.xml");
+        assert url != null;
+        File file = new File(url.getPath());
+        String input = PomHelper.readXml(file).getLeft();
+        Model model = new MavenXpp3Reader().read(new StringReader(input));
+        try (MutableXMLStreamReader pom = new MutableXMLStreamReader(file.toPath())) {
+            Map<String, String> implicitProperties = PomHelper.getImplicitProperties(pom, model);
+            assertThat(implicitProperties, hasEntry(is("projectVersion"), is("doNotOverride")));
+            assertThat(implicitProperties, hasEntry(is("propertyA"), is("propertyAValue")));
+            assertThat(implicitProperties, hasEntry(is("project.parent.groupId"), is("parentGroupId")));
+            assertThat(implicitProperties, hasEntry(is("project.parent.artifactId"), is("parentArtifactId")));
+            assertThat(implicitProperties, hasEntry(is("project.parent.version"), is("parentVersion")));
+            assertThat(implicitProperties, hasEntry(is("project.groupId"), is("parentGroupId")));
+            assertThat(implicitProperties, hasEntry(is("project.artifactId"), is("parentArtifactId")));
+            assertThat(implicitProperties, hasEntry(is("project.version"), is("parentVersion")));
+        }
+    }
+
+    static Stream<Arguments> testGetProjectVersionArguments() {
+        return Stream.of(
+                Arguments.of(
+                        "<project><modelVersion>4.0.0</modelVersion><parent/><version>projectVersion</version></project>",
+                        "projectVersion"),
+                Arguments.of("<project><modelVersion>4.0.0</modelVersion><parent/><version/></project>", ""));
+    }
+
+    @ParameterizedTest
+    @MethodSource("testGetProjectVersionArguments")
+    void testGetProjectVersion(String pom, String expectedValue)
+            throws XMLStreamException, IOException, TransformerException {
+        try (MutableXMLStreamReader reader = new MutableXMLStreamReader(toInputStream(pom, defaultCharset()), PATH)) {
+            assertThat(PomHelper.getProjectVersion(reader), is(expectedValue));
+        }
+    }
+
+    static Stream<Arguments> testSetProjectParentVersionArguments() {
+        return Stream.of(
+                Arguments.of(
+                        "<project><modelVersion>4.0.0</modelVersion><parent><version>parentVersion</version></parent><version>projectVersion</version></project>",
+                        true),
+                Arguments.of("<project><modelVersion>4.0.0</modelVersion><parent/><version/></project>", false));
+    }
+
+    @ParameterizedTest
+    @MethodSource("testSetProjectParentVersionArguments")
+    void testSetProjectParentVersion(String pom, boolean expectedValue)
+            throws XMLStreamException, IOException, TransformerException {
+        try (MutableXMLStreamReader reader = new MutableXMLStreamReader(toInputStream(pom, defaultCharset()), PATH)) {
+            assertThat(PomHelper.setProjectParentVersion(reader, "newVersion"), is(expectedValue));
+        }
+    }
+
+    @Test
+    void testSetDependencyVersion()
+            throws XMLStreamException, IOException, TransformerException, XmlPullParserException {
+        URL url = getClass().getResource("PomHelperTest.implicitProperties.pom.xml");
+        assert url != null;
+        File file = new File(url.getPath());
+        String input = PomHelper.readXml(file).getLeft();
+        Model model = new MavenXpp3Reader().read(new StringReader(input));
+        try (MutableXMLStreamReader pom = new MutableXMLStreamReader(file.toPath())) {
+            assertThat(
+                    PomHelper.setDependencyVersion(
+                            pom, "propertyAValue", "artifactA", "version", "newVersion", model, log),
+                    is(true));
+            Model newModel = new MavenXpp3Reader().read(new StringReader(pom.getSource()));
+            assertThat(
+                    newModel.getDependencies(),
+                    hasItem(allOf(
+                            hasProperty("groupId", is("${propertyA}")),
+                            hasProperty("artifactId", is("artifactA")),
+                            hasProperty("version", is("newVersion")))));
+        }
+    }
+
+    @Test
+    void testSetDependencyVersionDepMan()
+            throws XMLStreamException, IOException, TransformerException, XmlPullParserException {
+        URL url = getClass().getResource("PomHelperTest.implicitProperties.pom.xml");
+        assert url != null;
+        File file = new File(url.getPath());
+        String input = PomHelper.readXml(file).getLeft();
+        Model model = new MavenXpp3Reader().read(new StringReader(input));
+        try (MutableXMLStreamReader pom = new MutableXMLStreamReader(file.toPath())) {
+            assertThat(
+                    PomHelper.setDependencyVersion(pom, "groupB", "artifactB", "versionB", "newVersion", model, log),
+                    is(true));
+            Model newModel = new MavenXpp3Reader().read(new StringReader(pom.getSource()));
+            assertThat(
+                    newModel.getDependencyManagement().getDependencies(),
+                    hasItem(allOf(
+                            hasProperty("groupId", is("groupB")),
+                            hasProperty("artifactId", is("artifactB")),
+                            hasProperty("version", is("newVersion")))));
+        }
+    }
+
+    @Test
+    void testSetDependencyVersionProfile()
+            throws XMLStreamException, IOException, TransformerException, XmlPullParserException {
+        URL url = getClass().getResource("PomHelperTest.implicitProperties.pom.xml");
+        assert url != null;
+        File file = new File(url.getPath());
+        String input = PomHelper.readXml(file).getLeft();
+        Model model = new MavenXpp3Reader().read(new StringReader(input));
+        try (MutableXMLStreamReader pom = new MutableXMLStreamReader(file.toPath())) {
+            assertThat(
+                    PomHelper.setDependencyVersion(pom, "groupC", "artifactC", "versionC", "newVersion", model, log),
+                    is(true));
+            Model newModel = new MavenXpp3Reader().read(new StringReader(pom.getSource()));
+            assertThat(
+                    newModel.getProfiles().get(0).getDependencies(),
+                    hasItem(allOf(
+                            hasProperty("groupId", is("groupC")),
+                            hasProperty("artifactId", is("artifactC")),
+                            hasProperty("version", is("newVersion")))));
+        }
     }
 
     @Test
@@ -324,4 +500,25 @@ class PomHelperTest {
         assertThat(string, containsStringIgnoringCase("\"utf-16\""));
         assertThat(string, containsString("ąęśćńźĄŚĘŻĆ"));
     }
+
+    @Test
+    void testGetRawModelWithParents() throws IOException {
+        MavenProject grandfather = new MavenProject();
+        grandfather.getModel().setArtifactId("grandfather");
+
+        MavenProject father = new MavenProject();
+        father.setParent(grandfather);
+        grandfather.getModel().setArtifactId("father");
+
+        MavenProject child = new MavenProject();
+        child.setParent(father);
+        child.getModel().setArtifactId("child");
+
+        MavenProject grandchild = new MavenProject();
+        grandchild.setParent(child);
+        child.getModel().setArtifactId("grandchild");
+
+        Map<MavenProject, ?> map = PomHelper.getRawModelWithParents(grandchild);
+        assertThat(map.keySet(), contains(grandchild, child, father, grandfather));
+    }
 }
diff --git a/versions-common/src/test/java/org/codehaus/mojo/versions/api/PropertyVersionsBuilderTest.java b/versions-common/src/test/java/org/codehaus/mojo/versions/api/PropertyVersionsBuilderTest.java
index 39f4d086..03147b2c 100644
--- a/versions-common/src/test/java/org/codehaus/mojo/versions/api/PropertyVersionsBuilderTest.java
+++ b/versions-common/src/test/java/org/codehaus/mojo/versions/api/PropertyVersionsBuilderTest.java
@@ -1,8 +1,7 @@
 package org.codehaus.mojo.versions.api;
 
 import org.apache.maven.artifact.Artifact;
-import org.codehaus.mojo.versions.ordering.MavenVersionComparator;
-import org.codehaus.mojo.versions.ordering.VersionComparator;
+import org.apache.maven.plugin.logging.Log;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -13,12 +12,13 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.endsWith;
 import static org.hamcrest.Matchers.startsWith;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
 public class PropertyVersionsBuilderTest {
 
+    @Mock
+    private Log log;
+
     @Mock
     private VersionsHelper versionsHelper;
 
@@ -29,9 +29,7 @@ public class PropertyVersionsBuilderTest {
 
     @BeforeEach
     public void beforeEach() {
-        builder = new PropertyVersionsBuilder(versionsHelper, null, "property").withAssociation(artifact, false);
-        VersionComparator comparator = new MavenVersionComparator();
-        when(versionsHelper.getVersionComparator(any(Artifact.class))).thenReturn(comparator);
+        builder = new PropertyVersionsBuilder(versionsHelper, null, "property", log).withAssociation(artifact, false);
     }
 
     @ParameterizedTest
diff --git a/versions-common/src/test/resources/org/codehaus/mojo/versions/api/PomHelperTest.implicitProperties.pom.xml b/versions-common/src/test/resources/org/codehaus/mojo/versions/api/PomHelperTest.implicitProperties.pom.xml
new file mode 100644
index 00000000..5962d0db
--- /dev/null
+++ b/versions-common/src/test/resources/org/codehaus/mojo/versions/api/PomHelperTest.implicitProperties.pom.xml
@@ -0,0 +1,53 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>parentGroupId</groupId>
+        <artifactId>parentArtifactId</artifactId>
+        <version>parentVersion</version>
+    </parent>
+    <packaging>jar</packaging>
+    <name>set version for pom with no dependencies</name>
+    <properties>
+        <propertyA>propertyAValue</propertyA>
+        <projectVersion>doNotOverride</projectVersion>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>${propertyA}</groupId>
+            <version>version</version>
+            <artifactId>artifactA</artifactId>
+        </dependency>
+    </dependencies>
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>groupB</groupId>
+                <version>versionB</version>
+                <artifactId>artifactB</artifactId>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+    <profiles>
+        <profile>
+            <id>profileA</id>
+            <dependencies>
+                <dependency>
+                    <groupId>groupC</groupId>
+                    <artifactId>artifactC</artifactId>
+                    <version>versionC</version>
+                </dependency>
+            </dependencies>
+            <dependencyManagement>
+                <dependencies>
+                    <dependency>
+                        <groupId>groupD</groupId>
+                        <artifactId>artifactD</artifactId>
+                        <version>versionD</version>
+                    </dependency>
+                </dependencies>
+            </dependencyManagement>
+        </profile>
+    </profiles>
+</project>
diff --git a/versions-common/src/test/resources/org/codehaus/mojo/versions/api/PomHelperTest.profiles.pom.xml b/versions-common/src/test/resources/org/codehaus/mojo/versions/api/PomHelperTest.profiles.pom.xml
new file mode 100644
index 00000000..0a4c451e
--- /dev/null
+++ b/versions-common/src/test/resources/org/codehaus/mojo/versions/api/PomHelperTest.profiles.pom.xml
@@ -0,0 +1,23 @@
+<project>
+    <properties>
+        <propertyA>propertyAValue</propertyA>
+        <propertyB>propertyBValue</propertyB>
+    </properties>
+
+    <profiles>
+        <profile>
+            <id>profileA</id>
+            <properties>
+                <profileAPropertyA>profileAPropertyAValue</profileAPropertyA>
+                <profileAPropertyB>profileAPropertyBValue</profileAPropertyB>
+            </properties>
+        </profile>
+        <profile>
+            <id>profileB</id>
+            <properties>
+                <profileBPropertyA>profileBPropertyAValue</profileBPropertyA>
+                <profileBPropertyB>profileBPropertyBValue</profileBPropertyB>
+            </properties>
+        </profile>
+    </profiles>
+</project>
diff --git a/versions-enforcer/src/test/java/org/codehaus/mojo/versions/enforcer/MaxDependencyUpdatesTest.java b/versions-enforcer/src/test/java/org/codehaus/mojo/versions/enforcer/MaxDependencyUpdatesTest.java
index 7043b6f8..84eda59e 100644
--- a/versions-enforcer/src/test/java/org/codehaus/mojo/versions/enforcer/MaxDependencyUpdatesTest.java
+++ b/versions-enforcer/src/test/java/org/codehaus/mojo/versions/enforcer/MaxDependencyUpdatesTest.java
@@ -20,6 +20,7 @@ package org.codehaus.mojo.versions.enforcer;
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Properties;
 
 import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
 import org.apache.maven.enforcer.rule.api.EnforcerLogger;
@@ -73,6 +74,9 @@ class MaxDependencyUpdatesTest {
 
     @BeforeEach
     public void setup() {
+        Properties emptyProperties = new Properties();
+        when(mavenSession.getUserProperties()).thenReturn(emptyProperties);
+        when(mavenSession.getSystemProperties()).thenReturn(emptyProperties);
         artifactHandlerManager = mockArtifactHandlerManager();
         artifactFactory = new ArtifactFactory(artifactHandlerManager);
         maxDependencyUpdates = new MaxDependencyUpdates(
diff --git a/versions-maven-plugin/src/test/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojoTest.java b/versions-maven-plugin/src/test/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojoTest.java
index c50c412b..f59ea87e 100644
--- a/versions-maven-plugin/src/test/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojoTest.java
+++ b/versions-maven-plugin/src/test/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojoTest.java
@@ -60,8 +60,6 @@ public class DisplayExtensionUpdatesMojoTest {
     private DisplayExtensionUpdatesMojo mojo;
     private Path tempPath;
 
-    private PomHelper pomHelper;
-
     private ArtifactFactory artifactFactory;
 
     @Before
diff --git a/versions-test/src/main/java/org/codehaus/mojo/versions/utils/MockUtils.java b/versions-test/src/main/java/org/codehaus/mojo/versions/utils/MockUtils.java
index 39b48500..d612bbe9 100644
--- a/versions-test/src/main/java/org/codehaus/mojo/versions/utils/MockUtils.java
+++ b/versions-test/src/main/java/org/codehaus/mojo/versions/utils/MockUtils.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Properties;
 
 import org.apache.maven.artifact.Artifact;
 import org.apache.maven.artifact.handler.DefaultArtifactHandler;
@@ -184,6 +185,9 @@ public class MockUtils {
         MavenSession session = mock(MavenSession.class);
         when(session.getRepositorySession()).thenReturn(mock(RepositorySystemSession.class));
         when(session.getCurrentProject()).thenReturn(project);
+        Properties emptyProperties = new Properties();
+        when(session.getUserProperties()).thenReturn(emptyProperties);
+        when(session.getSystemProperties()).thenReturn(emptyProperties);
         return session;
     }
 }