[MNG-8081] Interpolate available properties during default profile selection (Maven 3.9.x) (#1447)

Co-authored-by: Guillaume Nodet <gnodet@gmail.com>
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
index 6ad8b8e..3043a76 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
@@ -26,23 +26,28 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Properties;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
 import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
 import org.apache.maven.artifact.versioning.VersionRange;
 import org.apache.maven.model.Activation;
-import org.apache.maven.model.ActivationFile;
 import org.apache.maven.model.Build;
 import org.apache.maven.model.Dependency;
 import org.apache.maven.model.DependencyManagement;
 import org.apache.maven.model.InputLocation;
+import org.apache.maven.model.InputLocationTracker;
 import org.apache.maven.model.InputSource;
 import org.apache.maven.model.Model;
 import org.apache.maven.model.Parent;
@@ -78,6 +83,7 @@
 import org.apache.maven.model.validation.ModelValidator;
 import org.codehaus.plexus.interpolation.InterpolationException;
 import org.codehaus.plexus.interpolation.MapBasedValueSource;
+import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
 import org.codehaus.plexus.interpolation.StringSearchInterpolator;
 import org.codehaus.plexus.util.StringUtils;
 import org.eclipse.sisu.Nullable;
@@ -299,14 +305,19 @@
 
             profileActivationContext.setProjectProperties(tmpModel.getProperties());
 
-            List<Profile> activePomProfiles =
-                    profileSelector.getActiveProfiles(rawModel.getProfiles(), profileActivationContext, problems);
-            currentData.setActiveProfiles(activePomProfiles);
-
             Map<String, Activation> interpolatedActivations =
                     getInterpolatedActivations(rawModel, profileActivationContext, problems);
             injectProfileActivations(tmpModel, interpolatedActivations);
 
+            List<Profile> activePomProfiles =
+                    profileSelector.getActiveProfiles(tmpModel.getProfiles(), profileActivationContext, problems);
+
+            Set<String> activeProfileIds =
+                    activePomProfiles.stream().map(Profile::getId).collect(Collectors.toSet());
+            currentData.setActiveProfiles(rawModel.getProfiles().stream()
+                    .filter(p -> activeProfileIds.contains(p.getId()))
+                    .collect(Collectors.toList()));
+
             // profile injection
             for (Profile activeProfile : activePomProfiles) {
                 profileInjector.injectProfile(tmpModel, activeProfile, request, problems);
@@ -413,42 +424,74 @@
         return result;
     }
 
+    @FunctionalInterface
+    private interface InterpolateString {
+        String apply(String s) throws InterpolationException;
+    }
+
     private Map<String, Activation> getInterpolatedActivations(
             Model rawModel, DefaultProfileActivationContext context, DefaultModelProblemCollector problems) {
         Map<String, Activation> interpolatedActivations = getProfileActivations(rawModel, true);
-        for (Activation activation : interpolatedActivations.values()) {
-            if (activation.getFile() != null) {
-                replaceWithInterpolatedValue(activation.getFile(), context, problems);
+
+        if (interpolatedActivations.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        RegexBasedInterpolator interpolator = new RegexBasedInterpolator();
+
+        interpolator.addValueSource(new MapBasedValueSource(context.getProjectProperties()));
+        interpolator.addValueSource(new MapBasedValueSource(context.getUserProperties()));
+        interpolator.addValueSource(new MapBasedValueSource(context.getSystemProperties()));
+
+        class Interpolation {
+            final InputLocationTracker target;
+
+            final InterpolateString impl;
+
+            Interpolation(InputLocationTracker target, InterpolateString impl) {
+                this.target = target;
+                this.impl = impl;
             }
+
+            void performFor(String value, String locationKey, Consumer<String> mutator) {
+                if (StringUtils.isEmpty(value)) {
+                    return;
+                }
+                try {
+                    mutator.accept(impl.apply(value));
+                } catch (InterpolationException e) {
+                    problems.add(new ModelProblemCollectorRequest(Severity.ERROR, Version.BASE)
+                            .setMessage("Failed to interpolate value " + value + ": " + e.getMessage())
+                            .setLocation(target.getLocation(locationKey))
+                            .setException(e));
+                }
+            }
+        }
+        for (Activation activation : interpolatedActivations.values()) {
+            Optional<Activation> a = Optional.of(activation);
+            a.map(Activation::getFile).ifPresent(fa -> {
+                Interpolation nt =
+                        new Interpolation(fa, s -> profileActivationFilePathInterpolator.interpolate(s, context));
+                nt.performFor(fa.getExists(), "exists", fa::setExists);
+                nt.performFor(fa.getMissing(), "missing", fa::setMissing);
+            });
+            a.map(Activation::getOs).ifPresent(oa -> {
+                Interpolation nt = new Interpolation(oa, interpolator::interpolate);
+                nt.performFor(oa.getArch(), "arch", oa::setArch);
+                nt.performFor(oa.getFamily(), "family", oa::setFamily);
+                nt.performFor(oa.getName(), "name", oa::setName);
+                nt.performFor(oa.getVersion(), "version", oa::setVersion);
+            });
+            a.map(Activation::getProperty).ifPresent(pa -> {
+                Interpolation nt = new Interpolation(pa, interpolator::interpolate);
+                nt.performFor(pa.getName(), "name", pa::setName);
+                nt.performFor(pa.getValue(), "value", pa::setValue);
+            });
+            a.map(Activation::getJdk).ifPresent(ja -> new Interpolation(activation, interpolator::interpolate)
+                    .performFor(ja, "jdk", activation::setJdk));
         }
         return interpolatedActivations;
     }
 
-    private void replaceWithInterpolatedValue(
-            ActivationFile activationFile, ProfileActivationContext context, DefaultModelProblemCollector problems) {
-        try {
-            if (StringUtils.isNotEmpty(activationFile.getExists())) {
-                String path = activationFile.getExists();
-                String absolutePath = profileActivationFilePathInterpolator.interpolate(path, context);
-                activationFile.setExists(absolutePath);
-            } else if (StringUtils.isNotEmpty(activationFile.getMissing())) {
-                String path = activationFile.getMissing();
-                String absolutePath = profileActivationFilePathInterpolator.interpolate(path, context);
-                activationFile.setMissing(absolutePath);
-            }
-        } catch (InterpolationException e) {
-            String path = StringUtils.isNotEmpty(activationFile.getExists())
-                    ? activationFile.getExists()
-                    : activationFile.getMissing();
-
-            problems.add(new ModelProblemCollectorRequest(Severity.ERROR, Version.BASE)
-                    .setMessage("Failed to interpolate file location " + path + ": " + e.getMessage())
-                    .setLocation(activationFile.getLocation(
-                            StringUtils.isNotEmpty(activationFile.getExists()) ? "exists" : "missing"))
-                    .setException(e));
-        }
-    }
-
     @Override
     public ModelBuildingResult build(ModelBuildingRequest request, ModelBuildingResult result)
             throws ModelBuildingException {
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java b/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
index 47a92ab..dc544d4 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
@@ -24,17 +24,24 @@
 
 import java.io.File;
 import java.util.Arrays;
+import java.util.Deque;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
+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.StreamSupport;
 
 import org.apache.maven.model.Activation;
-import org.apache.maven.model.ActivationFile;
 import org.apache.maven.model.Build;
 import org.apache.maven.model.BuildBase;
 import org.apache.maven.model.Dependency;
@@ -236,42 +243,97 @@
     }
 
     private void validate30RawProfileActivation(ModelProblemCollector problems, Activation activation, String prefix) {
-        if (activation == null || activation.getFile() == null) {
+        if (activation == null) {
             return;
         }
+        class ActivationFrame {
+            String location;
+            Optional<? extends InputLocationTracker> parent;
 
-        ActivationFile file = activation.getFile();
-
-        String path;
-        String location;
-
-        if (file.getExists() != null && !file.getExists().isEmpty()) {
-            path = file.getExists();
-            location = "exists";
-        } else if (file.getMissing() != null && !file.getMissing().isEmpty()) {
-            path = file.getMissing();
-            location = "missing";
-        } else {
-            return;
+            ActivationFrame(String location, Optional<? extends InputLocationTracker> parent) {
+                this.location = location;
+                this.parent = parent;
+            }
         }
+        final Deque<ActivationFrame> stk = new LinkedList<>();
 
-        if (hasProjectExpression(path)) {
-            Matcher matcher = EXPRESSION_PROJECT_NAME_PATTERN.matcher(path);
-            while (matcher.find()) {
-                String propertyName = matcher.group(0);
-                if (!"${project.basedir}".equals(propertyName)) {
+        final Supplier<String> pathSupplier = () -> {
+            final boolean parallel = false;
+            return StreamSupport.stream(((Iterable<ActivationFrame>) stk::descendingIterator).spliterator(), parallel)
+                    .map(f -> f.location)
+                    .collect(Collectors.joining("."));
+        };
+        final Supplier<InputLocation> locationSupplier = () -> {
+            if (stk.size() < 2) {
+                return null;
+            }
+            Iterator<ActivationFrame> f = stk.iterator();
+
+            String location = f.next().location;
+            ActivationFrame parent = f.next();
+
+            return parent.parent.map(p -> p.getLocation(location)).orElse(null);
+        };
+        final Consumer<String> validator = s -> {
+            if (hasProjectExpression(s)) {
+                String path = pathSupplier.get();
+                Matcher matcher = EXPRESSION_PROJECT_NAME_PATTERN.matcher(s);
+                while (matcher.find()) {
+                    String propertyName = matcher.group(0);
+
+                    if (path.startsWith("activation.file.") && "${project.basedir}".equals(propertyName)) {
+                        continue;
+                    }
                     addViolation(
                             problems,
                             Severity.WARNING,
                             Version.V30,
-                            prefix + "activation.file." + location,
+                            prefix + path,
                             null,
-                            "Failed to interpolate file location " + path + ": " + propertyName
+                            "Failed to interpolate profile activation property " + s + ": " + propertyName
                                     + " expressions are not supported during profile activation.",
-                            file.getLocation(location));
+                            locationSupplier.get());
                 }
             }
-        }
+        };
+        Optional<Activation> root = Optional.of(activation);
+        stk.push(new ActivationFrame("activation", root));
+        root.map(Activation::getFile).ifPresent(fa -> {
+            stk.push(new ActivationFrame("file", Optional.of(fa)));
+            stk.push(new ActivationFrame("exists", Optional.empty()));
+            validator.accept(fa.getExists());
+            stk.peek().location = "missing";
+            validator.accept(fa.getMissing());
+            stk.pop();
+            stk.pop();
+        });
+        root.map(Activation::getOs).ifPresent(oa -> {
+            stk.push(new ActivationFrame("os", Optional.of(oa)));
+            stk.push(new ActivationFrame("arch", Optional.empty()));
+            validator.accept(oa.getArch());
+            stk.peek().location = "family";
+            validator.accept(oa.getFamily());
+            stk.peek().location = "name";
+            validator.accept(oa.getName());
+            stk.peek().location = "version";
+            validator.accept(oa.getVersion());
+            stk.pop();
+            stk.pop();
+        });
+        root.map(Activation::getProperty).ifPresent(pa -> {
+            stk.push(new ActivationFrame("property", Optional.of(pa)));
+            stk.push(new ActivationFrame("name", Optional.empty()));
+            validator.accept(pa.getName());
+            stk.peek().location = "value";
+            validator.accept(pa.getValue());
+            stk.pop();
+            stk.pop();
+        });
+        root.map(Activation::getJdk).ifPresent(jdk -> {
+            stk.push(new ActivationFrame("jdk", Optional.empty()));
+            validator.accept(jdk);
+            stk.pop();
+        });
     }
 
     private void validate20RawPlugins(
diff --git a/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java b/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
index 8a7b4fe..22a94db 100644
--- a/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
+++ b/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
@@ -20,6 +20,8 @@
 
 import java.io.InputStream;
 import java.util.List;
+import java.util.Properties;
+import java.util.function.UnaryOperator;
 
 import junit.framework.TestCase;
 import org.apache.maven.model.Model;
@@ -44,29 +46,41 @@
     }
 
     private SimpleProblemCollector validate(String pom) throws Exception {
-        return validateEffective(pom, ModelBuildingRequest.VALIDATION_LEVEL_STRICT);
+        return validateEffective(pom, UnaryOperator.identity());
     }
 
     private SimpleProblemCollector validateRaw(String pom) throws Exception {
-        return validateRaw(pom, ModelBuildingRequest.VALIDATION_LEVEL_STRICT);
+        return validateRaw(pom, UnaryOperator.identity());
     }
 
     private SimpleProblemCollector validateEffective(String pom, int level) throws Exception {
-        ModelBuildingRequest request = new DefaultModelBuildingRequest().setValidationLevel(level);
+        return validateEffective(pom, mbr -> mbr.setValidationLevel(level));
+    }
 
-        SimpleProblemCollector problems = new SimpleProblemCollector(read(pom));
+    private SimpleProblemCollector validateEffective(String pom, UnaryOperator<ModelBuildingRequest> requestConfigurer)
+            throws Exception {
+        Model model = read(pom);
 
-        validator.validateEffectiveModel(problems.getModel(), request, problems);
+        SimpleProblemCollector problems = new SimpleProblemCollector(model);
+
+        validator.validateEffectiveModel(model, requestConfigurer.apply(new DefaultModelBuildingRequest()), problems);
 
         return problems;
     }
 
     private SimpleProblemCollector validateRaw(String pom, int level) throws Exception {
-        ModelBuildingRequest request = new DefaultModelBuildingRequest().setValidationLevel(level);
+        return validateRaw(pom, mbr -> mbr.setValidationLevel(level));
+    }
 
-        SimpleProblemCollector problems = new SimpleProblemCollector(read(pom));
+    private SimpleProblemCollector validateRaw(String pom, UnaryOperator<ModelBuildingRequest> requestConfigurer)
+            throws Exception {
+        Model model = read(pom);
 
-        validator.validateRawModel(problems.getModel(), request, problems);
+        SimpleProblemCollector problems = new SimpleProblemCollector(model);
+
+        ModelBuildingRequest request = requestConfigurer.apply(new DefaultModelBuildingRequest());
+
+        validator.validateRawModel(model, request, problems);
 
         return problems;
     }
@@ -748,24 +762,61 @@
                 result.getWarnings().get(0));
     }
 
-    public void testProfileActivationWithAllowedExpression() throws Exception {
-        SimpleProblemCollector result = validateRaw("raw-model/profile-activation-file-with-allowed-expressions.xml");
+    public void testRepositoryWithExpression() throws Exception {
+        SimpleProblemCollector result = validateRaw("raw-model/repository-with-expression.xml");
         assertViolations(result, 0, 0, 0);
     }
 
-    public void testProfileActivationWithProjectExpression() throws Exception {
+    public void testRepositoryWithBasedirExpression() throws Exception {
+        SimpleProblemCollector result = validateRaw("raw-model/repository-with-basedir-expression.xml");
+        assertViolations(result, 0, 0, 0);
+    }
+
+    public void testProfileActivationWithAllowedExpression() throws Exception {
+        SimpleProblemCollector result = validateRaw(
+                "raw-model/profile-activation-file-with-allowed-expressions.xml",
+                mbr -> mbr.setUserProperties(new Properties() {
+                    private static final long serialVersionUID = 1L;
+
+                    {
+                        setProperty("foo", "foo");
+                        setProperty("bar", "foo");
+                    }
+                }));
+        assertViolations(result, 0, 0, 0);
+    }
+
+    public void testProfileActivationFileWithProjectExpression() throws Exception {
         SimpleProblemCollector result = validateRaw("raw-model/profile-activation-file-with-project-expressions.xml");
         assertViolations(result, 0, 0, 2);
 
         assertEquals(
                 "'profiles.profile[exists-project-version].activation.file.exists' "
-                        + "Failed to interpolate file location ${project.version}/test.txt: "
+                        + "Failed to interpolate profile activation property ${project.version}/test.txt: "
                         + "${project.version} expressions are not supported during profile activation.",
                 result.getWarnings().get(0));
 
         assertEquals(
                 "'profiles.profile[missing-project-version].activation.file.missing' "
-                        + "Failed to interpolate file location ${project.version}/test.txt: "
+                        + "Failed to interpolate profile activation property ${project.version}/test.txt: "
+                        + "${project.version} expressions are not supported during profile activation.",
+                result.getWarnings().get(1));
+    }
+
+    public void testProfileActivationPropertyWithProjectExpression() throws Exception {
+        SimpleProblemCollector result =
+                validateRaw("raw-model/profile-activation-property-with-project-expressions.xml");
+        assertViolations(result, 0, 0, 2);
+
+        assertEquals(
+                "'profiles.profile[property-name-project-version].activation.property.name' "
+                        + "Failed to interpolate profile activation property ${project.version}: "
+                        + "${project.version} expressions are not supported during profile activation.",
+                result.getWarnings().get(0));
+
+        assertEquals(
+                "'profiles.profile[property-value-project-version].activation.property.value' "
+                        + "Failed to interpolate profile activation property ${project.version}: "
                         + "${project.version} expressions are not supported during profile activation.",
                 result.getWarnings().get(1));
     }
diff --git a/maven-model-builder/src/test/resources/poms/validation/raw-model/profile-activation-file-with-allowed-expressions.xml b/maven-model-builder/src/test/resources/poms/validation/raw-model/profile-activation-file-with-allowed-expressions.xml
index a4beb62..72b6747 100644
--- a/maven-model-builder/src/test/resources/poms/validation/raw-model/profile-activation-file-with-allowed-expressions.xml
+++ b/maven-model-builder/src/test/resources/poms/validation/raw-model/profile-activation-file-with-allowed-expressions.xml
@@ -60,5 +60,23 @@
       </activation>
     </profile>
 
+    <profile>
+      <id>dynamic-property-available</id>
+      <activation>
+        <property>
+          <name>${activationProperty}</name>
+        </property>
+      </activation>
+    </profile>
+
+    <profile>
+      <id>matches-another-property</id>
+      <activation>
+        <property>
+          <name>foo</name>
+          <value>${bar}</value>
+        </property>
+      </activation>
+    </profile>
   </profiles>
 </project>
diff --git a/maven-model-builder/src/test/resources/poms/validation/raw-model/profile-activation-property-with-project-expressions.xml b/maven-model-builder/src/test/resources/poms/validation/raw-model/profile-activation-property-with-project-expressions.xml
new file mode 100644
index 0000000..8bcf89f
--- /dev/null
+++ b/maven-model-builder/src/test/resources/poms/validation/raw-model/profile-activation-property-with-project-expressions.xml
@@ -0,0 +1,49 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <artifactId>aid</artifactId>
+  <groupId>gid</groupId>
+  <version>0.1</version>
+  <packaging>pom</packaging>
+
+  <profiles>
+
+    <profile>
+      <id>property-name-project-version</id>
+      <activation>
+        <property>
+          <name>${project.version}</name>
+        </property>
+      </activation>
+    </profile>
+    <profile>
+      <id>property-value-project-version</id>
+      <activation>
+        <property>
+          <name>project.version</name>
+          <value>${project.version}</value>
+        </property>
+      </activation>
+    </profile>
+
+  </profiles>
+</project>
diff --git a/maven-model-builder/src/test/resources/poms/validation/raw-model/repository-with-basedir-expression.xml b/maven-model-builder/src/test/resources/poms/validation/raw-model/repository-with-basedir-expression.xml
new file mode 100644
index 0000000..2dd11eb
--- /dev/null
+++ b/maven-model-builder/src/test/resources/poms/validation/raw-model/repository-with-basedir-expression.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<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/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.maven.validation</groupId>
+    <artifactId>parent</artifactId>
+    <version>1</version>
+  </parent>
+
+  <groupId>org.apache.maven.validation</groupId>
+  <artifactId>project</artifactId>
+  <version>1.0.0-SNAPSHOT</version>
+
+  <repositories>
+    <repository>
+      <id>repo</id>
+      <url>file://${basedir}/target/remote-repo</url>
+    </repository>
+    <repository>
+      <id>repo-project-basedir</id>
+      <url>file://${project.basedir}/sdk/maven/repo</url>
+    </repository>
+    <repository>
+      <id>repo-project-baseUri</id>
+      <url>${project.baseUri}/sdk/maven/repo</url>
+    </repository>
+  </repositories>
+
+</project>
diff --git a/maven-model-builder/src/test/resources/poms/validation/raw-model/repository-with-expression.xml b/maven-model-builder/src/test/resources/poms/validation/raw-model/repository-with-expression.xml
new file mode 100644
index 0000000..fcdd946
--- /dev/null
+++ b/maven-model-builder/src/test/resources/poms/validation/raw-model/repository-with-expression.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<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/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.maven.validation</groupId>
+    <artifactId>parent</artifactId>
+    <version>1</version>
+  </parent>
+
+  <groupId>org.apache.maven.validation</groupId>
+  <artifactId>project</artifactId>
+  <version>1.0.0-SNAPSHOT</version>
+
+  <properties>
+    <x>just/some/path</x>
+  </properties>
+
+  <repositories>
+    <repository>
+      <id>repo</id>
+      <url>file://${x}/sdk/maven/repo</url>
+    </repository>
+  </repositories>
+
+</project>
\ No newline at end of file