From 55055e203277c877a7cbbf2b52da697222123dbd Mon Sep 17 00:00:00 2001 From: Vincent Potucek Date: Fri, 6 Jun 2025 13:08:28 +0200 Subject: [PATCH] fix: raw exception catch in DefaultModelInterpolator --- .../impl/model/DefaultModelInterpolator.java | 64 +-- .../model/DefaultModelInterpolatorTest.java | 441 +++++++++++++++++- 2 files changed, 447 insertions(+), 58 deletions(-) diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelInterpolator.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelInterpolator.java index 6194c1289f61..1bf329451e9e 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelInterpolator.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelInterpolator.java @@ -23,12 +23,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.BinaryOperator; -import java.util.function.Function; -import java.util.function.UnaryOperator; import org.apache.maven.api.di.Inject; import org.apache.maven.api.di.Named; @@ -44,6 +40,7 @@ import org.apache.maven.api.services.model.PathTranslator; import org.apache.maven.api.services.model.RootLocator; import org.apache.maven.api.services.model.UrlNormalizer; +import org.apache.maven.impl.model.reflection.IntrospectionException; import org.apache.maven.impl.model.reflection.ReflectionValueExtractor; import org.apache.maven.model.v4.MavenTransformer; @@ -101,21 +98,21 @@ interface InnerInterpolator { @Override public Model interpolateModel( Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems) { - InnerInterpolator innerInterpolator = createInterpolator(model, projectDir, request, problems); - return new MavenTransformer(innerInterpolator::interpolate).visit(model); + return new MavenTransformer(createInterpolator(model, projectDir, request, problems)::interpolate).visit(model); } private InnerInterpolator createInterpolator( Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems) { - - Map> cache = new HashMap<>(); - Function> ucb = - v -> Optional.ofNullable(callback(model, projectDir, request, problems, v)); - UnaryOperator cb = v -> cache.computeIfAbsent(v, ucb).orElse(null); - BinaryOperator postprocessor = (e, v) -> postProcess(projectDir, request, e, v); return value -> { try { - return interpolator.interpolate(value, cb, postprocessor, false); + return interpolator.interpolate( + value, + v -> new HashMap>() + .computeIfAbsent( + v, v2 -> Optional.ofNullable(doCallback(model, projectDir, request, v2))) + .orElse(null), + (e, v1) -> postProcess(projectDir, request, e, v1), + false); } catch (InterpolatorException e) { problems.add(BuilderProblem.Severity.ERROR, ModelProblem.Version.BASE, e.getMessage(), e); return null; @@ -129,23 +126,9 @@ protected List getProjectPrefixes(ModelBuilderRequest request) { : PROJECT_PREFIXES_3_1; } - String callback( - Model model, - Path projectDir, - ModelBuilderRequest request, - ModelProblemCollector problems, - String expression) { - String value = doCallback(model, projectDir, request, problems, expression); - if (value != null) { - // value = postProcess(projectDir, request, expression, value); - } - return value; - } - private String postProcess(Path projectDir, ModelBuilderRequest request, String expression, String value) { // path translation - String exp = unprefix(expression, getProjectPrefixes(request)); - if (TRANSLATED_PATH_EXPRESSIONS.contains(exp)) { + if (TRANSLATED_PATH_EXPRESSIONS.contains(unprefix(expression, getProjectPrefixes(request)))) { value = pathTranslator.alignToBaseDirectory(value, projectDir); } // normalize url @@ -164,12 +147,7 @@ private String unprefix(String expression, List prefixes) { return expression; } - String doCallback( - Model model, - Path projectDir, - ModelBuilderRequest request, - ModelProblemCollector problems, - String expression) { + String doCallback(Model model, Path projectDir, ModelBuilderRequest request, String expression) { // basedir (the prefixed combos are handled below) if ("basedir".equals(expression)) { return projectProperty(model, projectDir, expression, false); @@ -220,8 +198,7 @@ String projectProperty(Model model, Path projectDir, String subExpr, boolean pre if (value != null) { return value.toString(); } - } catch (Exception e) { - // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e); + } catch (IntrospectionException ignored) { } } else if (prefixed && subExpr.equals("baseUri")) { return projectDir.toAbsolutePath().toUri().toASCIIString(); @@ -232,8 +209,7 @@ String projectProperty(Model model, Path projectDir, String subExpr, boolean pre if (value != null) { return value.toString(); } - } catch (Exception e) { - // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e); + } catch (IntrospectionException ignored) { } } else if (prefixed && subExpr.equals("rootDirectory")) { return rootLocator.findMandatoryRoot(projectDir).toString(); @@ -244,19 +220,15 @@ String projectProperty(Model model, Path projectDir, String subExpr, boolean pre if (value != null) { return value.toString(); } - } catch (Exception e) { - // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e); + } catch (IntrospectionException ignored) { } } } try { Object value = ReflectionValueExtractor.evaluate(subExpr, model, false); - if (value != null) { - return value.toString(); - } - } catch (Exception e) { - // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e); + return value != null ? value.toString() : null; + } catch (IntrospectionException ignored) { + return null; } - return null; } } diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelInterpolatorTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelInterpolatorTest.java index 7afe7a82e2de..084001a57de6 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelInterpolatorTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelInterpolatorTest.java @@ -18,19 +18,23 @@ */ package org.apache.maven.impl.model; +import java.lang.reflect.Method; +import java.net.URI; import java.nio.file.FileSystem; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; +import java.time.LocalDate; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.TimeZone; import java.util.concurrent.atomic.AtomicReference; @@ -41,28 +45,43 @@ import org.apache.maven.api.di.Provides; import org.apache.maven.api.model.Build; import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.DistributionManagement; import org.apache.maven.api.model.Model; import org.apache.maven.api.model.Organization; import org.apache.maven.api.model.Repository; import org.apache.maven.api.model.Resource; import org.apache.maven.api.model.Scm; +import org.apache.maven.api.model.Site; +import org.apache.maven.api.services.Interpolator; import org.apache.maven.api.services.Lookup; import org.apache.maven.api.services.ModelBuilderRequest; import org.apache.maven.api.services.model.ModelInterpolator; +import org.apache.maven.api.services.model.PathTranslator; import org.apache.maven.api.services.model.RootLocator; +import org.apache.maven.api.services.model.UrlNormalizer; +import org.apache.maven.impl.DefaultUrlNormalizer; import org.apache.maven.impl.model.profile.SimpleProblemCollector; +import org.apache.maven.impl.model.rootlocator.DefaultRootLocator; import org.apache.maven.impl.standalone.ApiRunner; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledOnOs; +import static org.apache.maven.api.services.ModelBuilderRequest.RequestType.BUILD_PROJECT; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.condition.OS.WINDOWS; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -/** - */ class DefaultModelInterpolatorTest { Map context; @@ -476,6 +495,8 @@ public void expressionThatEvaluatesToNullReturnsTheLiteralString() throws Except @Test public void shouldInterpolateSourceDirectoryReferencedFromResourceDirectoryCorrectly() throws Exception { + final SimpleProblemCollector collector = new SimpleProblemCollector(); + assertCollectorState(0, 0, 0, collector); Model model = Model.newBuilder() .build(Build.newBuilder() .sourceDirectory("correct") @@ -484,16 +505,16 @@ public void shouldInterpolateSourceDirectoryReferencedFromResourceDirectoryCorre .build())) .build()) .build(); - - final SimpleProblemCollector collector = new SimpleProblemCollector(); - Model out = interpolator.interpolateModel( - model, null, createModelBuildingRequest(context).build(), collector); + String sourceDirectory = interpolator + .interpolateModel( + model, null, createModelBuildingRequest(context).build(), collector) + .getBuild() + .getResources() + .iterator() + .next() + .getDirectory(); + assertEquals(model.getBuild().getSourceDirectory(), sourceDirectory); assertCollectorState(0, 0, 0, collector); - - List outResources = out.getBuild().getResources(); - Iterator resIt = outResources.iterator(); - - assertEquals(model.getBuild().getSourceDirectory(), resIt.next().getDirectory()); } @Test @@ -592,4 +613,400 @@ public Path findMandatoryRoot(Path basedir) { } }; } + + @Test + @DisabledOnOs(WINDOWS) + void testProjectPropertyExtraction() throws Exception { + Path projectDir = Paths.get("/test/path"); + Model model = Model.newBuilder().build(); + DefaultModelInterpolator interpolator = new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()); + assertEquals("/test/path", interpolator.projectProperty(model, projectDir, "basedir", false)); + assertNull(interpolator.projectProperty(model, projectDir, "nonexistent.property", false)); + } + + @Test + @EnabledOnOs(WINDOWS) + void testProjectPropertyExtractionOnWindows() throws Exception { + Path projectDir = Paths.get("/test/path"); + Model model = Model.newBuilder().build(); + DefaultModelInterpolator interpolator = new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()); + assertEquals("D:\\test\\path", interpolator.projectProperty(model, projectDir, "basedir", false)); + assertNull(interpolator.projectProperty(model, projectDir, "nonexistent.property", false)); + } + + @Test + void testPathHandling() { + Path projectDir = Jimfs.newFileSystem(Configuration.windows()).getPath("test/path"); + Model model = Model.newBuilder() + .build(Build.newBuilder() + .directory("${project.basedir}\\target") + .build()) + .build(); + Model out = interpolator.interpolateModel( + model, + projectDir, + createModelBuildingRequest(Collections.emptyMap()).build(), + new SimpleProblemCollector()); + assertEquals("C:\\work\\test\\path\\target", out.getBuild().getDirectory()); + } + + @Test + void testWindowsPathHandling() { + Path projectDir = Jimfs.newFileSystem(Configuration.windows()).getPath("C:\\test\\path"); + Model model = Model.newBuilder() + .build(Build.newBuilder() + .directory("${project.basedir}\\target") + .build()) + .build(); + Model out = interpolator.interpolateModel( + model, + projectDir, + createModelBuildingRequest(Collections.emptyMap()).build(), + new SimpleProblemCollector()); + String expected = projectDir.resolve("target").toString(); + assertEquals(expected, out.getBuild().getDirectory()); + } + + @Test + void testBasedirPropertyExtraction() throws Exception { + Path testDir = Paths.get("/test/path").toAbsolutePath(); + assertEquals( + testDir.toString(), + new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()) + .projectProperty(Model.newBuilder().build(), testDir, "basedir", false)); + } + + @Test + void testBasedirPrefixedPropertyExtraction() throws Exception { + Path testDir = Paths.get("/test/path").toAbsolutePath(); + Model model = Model.newBuilder().build(); + DefaultModelInterpolator interpolator = new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()); + assertEquals( + testDir.getParent().toString(), interpolator.projectProperty(model, testDir, "basedir.parent", false)); + assertEquals( + testDir.getFileName().toString(), + interpolator.projectProperty(model, testDir, "basedir.fileName", false)); + } + + @Test + void testBasedirPropertyWithNullProjectDir() throws Exception { + Model model = Model.newBuilder().build(); + DefaultModelInterpolator interpolator = new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()); + assertNull(interpolator.projectProperty(model, null, "basedir", false)); + assertNull(interpolator.projectProperty(model, null, "basedir.any", false)); + } + + @Test + void testBaseUriPropertyExtraction() throws Exception { + Path testDir = Paths.get("/test/path").toAbsolutePath(); + URI testUri = testDir.toUri(); + Model model = Model.newBuilder().build(); + DefaultModelInterpolator interpolator = new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()); + assertEquals(testUri.toASCIIString(), interpolator.projectProperty(model, testDir, "baseUri", true)); + } + + @Test + void testBaseUriPropertyWithNullProjectDir() throws Exception { + Model model = Model.newBuilder().build(); + DefaultModelInterpolator interpolator = new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()); + assertNull(interpolator.projectProperty(model, null, "baseUri", true)); + assertNull(interpolator.projectProperty(model, null, "baseUri.any", true)); + } + + @Test + void testBaseUriPropertyWithoutPrefix() throws Exception { + Path testDir = Paths.get("/test/path").toAbsolutePath(); + Model model = Model.newBuilder().build(); + DefaultModelInterpolator interpolator = new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()); + assertNull(interpolator.projectProperty(model, testDir, "baseUri", false)); + assertNull(interpolator.projectProperty(model, testDir, "baseUri.path", false)); + } + + @Test + void testBuildTimestampInterpolation() { + Map props = new HashMap<>(); + props.put("maven.build.timestamp.format", "yyyy-MM-dd"); + + Model model = Model.newBuilder().properties(props).build(); + ModelBuilderRequest request = createModelBuildingRequest(Collections.emptyMap()) + .session(session) + .build(); + + DefaultModelInterpolator interpolator = new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()); + + String expectedDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + + assertEquals(expectedDate, interpolator.doCallback(model, null, request, "build.timestamp")); + assertEquals(expectedDate, interpolator.doCallback(model, null, request, "maven.build.timestamp")); + } + + @Test + void testBuildTimestampWithDefaultFormat() { + assertThat(Math.abs(Instant.parse(new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()) + .doCallback( + Model.newBuilder().build(), + null, + createModelBuildingRequest(Collections.emptyMap()) + .session(session) + .build(), + "build.timestamp")) + .getEpochSecond() + - Instant.now().getEpochSecond())) + .isLessThan(3); + } + + @Test + void testNonTimestampExpressions() { + Model model = Model.newBuilder().build(); + ModelBuilderRequest request = createModelBuildingRequest(Collections.emptyMap()) + .session(session) + .build(); + DefaultModelInterpolator interpolator = new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()); + assertNull(interpolator.doCallback(model, null, request, "not.a.timestamp")); + assertNull(interpolator.doCallback(model, null, request, "build.something.else")); + } + + @Test + void testBaseUriPropertyExtractionWithReflection() throws Exception { + Path testDir = Paths.get("/test/path").toAbsolutePath(); + URI testUri = testDir.toUri(); + + Model model = Model.newBuilder().build(); + DefaultModelInterpolator interpolator = new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()); + + // Test direct baseUri access + assertEquals(testUri.toASCIIString(), interpolator.projectProperty(model, testDir, "baseUri", true)); + + // Test URI path component + assertEquals(testUri.getPath(), interpolator.projectProperty(model, testDir, "baseUri.path", true)); + + // Test URI scheme component + assertEquals(testUri.getScheme(), interpolator.projectProperty(model, testDir, "baseUri.scheme", true)); + + // Test URI host component + assertEquals(testUri.getHost(), interpolator.projectProperty(model, testDir, "baseUri.host", true)); + + // Test non-existent URI property + assertNull(interpolator.projectProperty(model, testDir, "baseUri.nonexistent", true)); + + // Test with null project fs + assertNull(interpolator.projectProperty(model, null, "baseUri.path", true)); + } + + @Test + void testUrlNormalizationThroughInterpolation() { + // Setup + DefaultUrlNormalizer urlNormalizer = new DefaultUrlNormalizer(); + DefaultPathTranslator pathTranslator = new DefaultPathTranslator(); + DefaultRootLocator rootLocator = new DefaultRootLocator(); + DefaultInterpolator interpolator = new DefaultInterpolator(); + + DefaultModelInterpolator modelInterpolator = + new DefaultModelInterpolator(pathTranslator, urlNormalizer, rootLocator, interpolator); + + Path projectDir = Paths.get("/test/path"); + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(BUILD_PROJECT) + .build(); + + SimpleProblemCollector problems = new SimpleProblemCollector(); + + // Test project.url normalization + Model urlModel = Model.newBuilder().url("http://example.com/../path").build(); + Model interpolatedUrlModel = modelInterpolator.interpolateModel(urlModel, projectDir, request, problems); + assertEquals("http://example.com/../path", interpolatedUrlModel.getUrl()); + + // Test project.scm.url normalization + Model scmModel = Model.newBuilder() + .scm(Scm.newBuilder().url("http://example.com/scm/../path").build()) + .build(); + Model interpolatedScmModel = modelInterpolator.interpolateModel(scmModel, projectDir, request, problems); + assertEquals( + "http://example.com/scm/../path", interpolatedScmModel.getScm().getUrl()); + + // Test project.scm.connection normalization + Model scmConnectionModel = Model.newBuilder() + .scm(Scm.newBuilder() + .connection("scm:git:http://example.com/repo/../path.git") + .build()) + .build(); + Model interpolatedScmConnectionModel = + modelInterpolator.interpolateModel(scmConnectionModel, projectDir, request, problems); + assertEquals( + "scm:git:http://example.com/repo/../path.git", + interpolatedScmConnectionModel.getScm().getConnection()); + + // Test project.distributionManagement.site.url normalization + Model siteModel = Model.newBuilder() + .distributionManagement(DistributionManagement.newBuilder() + .site(Site.newBuilder() + .url("http://example.com/site/../path") + .build()) + .build()) + .build(); + Model interpolatedSiteModel = modelInterpolator.interpolateModel(siteModel, projectDir, request, problems); + assertEquals( + "http://example.com/site/../path", + interpolatedSiteModel.getDistributionManagement().getSite().getUrl()); + + // Verify no problems were reported + assertTrue(problems.getErrors().isEmpty()); + assertTrue(problems.getWarnings().isEmpty()); + assertTrue(problems.getFatals().isEmpty()); + } + + @Test + void testProjectPrefixes31Interpolation() { + // Setup + DefaultModelInterpolator interpolator = new DefaultModelInterpolator( + new DefaultPathTranslator(), + new DefaultUrlNormalizer(), + new DefaultRootLocator(), + new DefaultInterpolator()); + + Path projectDir = Paths.get("/test/path"); + + // Create request with RequestType that should use PROJECT_PREFIXES_3_1 + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER) // Use PROJECT instead of OTHER + .build(); + + SimpleProblemCollector problems = new SimpleProblemCollector(); + + // Create model with properties to test both pom. and project. prefixes + Model model = Model.newBuilder() + .version("1.0") + .artifactId("test-artifact") + .properties(Map.of( + "testProp", "${pom.version}", + "testProp2", "${project.version}", + "testProp3", "${pom.artifactId}", + "testProp4", "${project.artifactId}")) + .build(); + + // Execute interpolation + Model result = interpolator.interpolateModel(model, projectDir, request, problems); + + // Verify both pom. and project. prefixes work + assertEquals("1.0", result.getProperties().get("testProp")); + assertEquals("1.0", result.getProperties().get("testProp2")); + assertEquals("test-artifact", result.getProperties().get("testProp3")); + assertEquals("test-artifact", result.getProperties().get("testProp4")); + + // Verify no problems were reported + assertTrue(problems.getErrors().isEmpty()); + assertTrue(problems.getWarnings().isEmpty()); + assertTrue(problems.getFatals().isEmpty()); + + // Verify the correct prefixes list is used + assertEquals(Arrays.asList("pom.", "project."), interpolator.getProjectPrefixes(request)); + } + + private static final Set URL_EXPRESSIONS = Set.of( + "project.url", + "project.scm.url", + "project.scm.connection", + "project.scm.developerConnection", + "project.distributionManagement.site.url"); + + @Test + void testUrlNormalizationInPostProcess() throws Exception { + PathTranslator mockPathTranslator = new DefaultPathTranslator(); + UrlNormalizer mockUrlNormalizer = spy(new DefaultUrlNormalizer()); + RootLocator mockRootLocator = new DefaultRootLocator(); + Interpolator mockInterpolator = new DefaultInterpolator(); + + // Create test instance with mocks + DefaultModelInterpolator interpolator = + new DefaultModelInterpolator(mockPathTranslator, mockUrlNormalizer, mockRootLocator, mockInterpolator); + + // Prepare test data + Path projectDir = Paths.get("/test/path"); + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .build(); + + // Test URL expressions that should be normalized + for (String urlExpression : URL_EXPRESSIONS) { + String testValue = "http://example.com/../test"; + String expectedNormalized = "http://example.com/test"; + + // Configure mock to return normalized value + when(mockUrlNormalizer.normalize(testValue)).thenReturn(expectedNormalized); + + // Call private method using reflection + Method postProcess = DefaultModelInterpolator.class.getDeclaredMethod( + "postProcess", Path.class, ModelBuilderRequest.class, String.class, String.class); + postProcess.setAccessible(true); + + String result = (String) postProcess.invoke(interpolator, projectDir, request, urlExpression, testValue); + + assertEquals(expectedNormalized, result); + } + + // Test non-URL expression that should NOT be normalized + String nonUrlExpression = "project.name"; + String nonUrlValue = "http://example.com/../should-not-normalize"; + + Method postProcess = DefaultModelInterpolator.class.getDeclaredMethod( + "postProcess", Path.class, ModelBuilderRequest.class, String.class, String.class); + postProcess.setAccessible(true); + + String result = (String) postProcess.invoke(interpolator, projectDir, request, nonUrlExpression, nonUrlValue); + + // Verify normalization was NOT called + verify(mockUrlNormalizer, never()).normalize(nonUrlValue); + assertEquals(nonUrlValue, result); + } }