diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java index 871731fb17e1..e4143928d5bf 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java @@ -539,6 +539,16 @@ public final class Constants { public static final String MAVEN_VERSION_RANGE_RESOLVER_NATURE_OVERRIDE = "maven.versionRangeResolver.natureOverride"; + /** + * Comma-separated list of XML contexts/fields to intern during POM parsing for memory optimization. + * When not specified, a default set of commonly repeated contexts will be used. + * Example: "groupId,artifactId,version,scope,type" + * + * @since 4.0.0 + */ + @Config + public static final String MAVEN_MODEL_BUILDER_INTERNS = "maven.modelBuilder.interns"; + /** * All system properties used by Maven Logger start with this prefix. * diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultModelXmlFactory.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultModelXmlFactory.java index e7c9cf884bfa..f447627cc794 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultModelXmlFactory.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultModelXmlFactory.java @@ -92,7 +92,9 @@ private Model doRead(XmlReaderRequest request) throws XmlReaderException { source = new InputSource( request.getModelId(), path != null ? path.toUri().toString() : null); } - MavenStaxReader xml = new MavenStaxReader(); + MavenStaxReader xml = request.getTransformer() != null + ? new MavenStaxReader(request.getTransformer()::transform) + : new MavenStaxReader(); xml.setAddDefaultEntities(request.isAddDefaultEntities()); if (inputStream != null) { return xml.read(inputStream, request.isStrict(), source); diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultPluginXmlFactory.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultPluginXmlFactory.java index f6dc37400e97..c287a7d91422 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultPluginXmlFactory.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultPluginXmlFactory.java @@ -56,7 +56,9 @@ public PluginDescriptor read(@Nonnull XmlReaderRequest request) throws XmlReader throw new IllegalArgumentException("path, url, reader or inputStream must be non null"); } try { - PluginDescriptorStaxReader xml = new PluginDescriptorStaxReader(); + PluginDescriptorStaxReader xml = request.getTransformer() != null + ? new PluginDescriptorStaxReader(request.getTransformer()::transform) + : new PluginDescriptorStaxReader(); xml.setAddDefaultEntities(request.isAddDefaultEntities()); if (inputStream != null) { return xml.read(inputStream, request.isStrict()); diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsXmlFactory.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsXmlFactory.java index fd1749cd0e06..348c8a9a8b85 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsXmlFactory.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsXmlFactory.java @@ -55,7 +55,9 @@ public Settings read(@Nonnull XmlReaderRequest request) throws XmlReaderExceptio if (request.getModelId() != null || request.getLocation() != null) { source = new InputSource(request.getLocation()); } - SettingsStaxReader xml = new SettingsStaxReader(); + SettingsStaxReader xml = request.getTransformer() != null + ? new SettingsStaxReader(request.getTransformer()::transform) + : new SettingsStaxReader(); xml.setAddDefaultEntities(request.isAddDefaultEntities()); if (reader != null) { return xml.read(reader, request.isStrict(), source); diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainsXmlFactory.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainsXmlFactory.java index 2db24aa8ec0f..18a6bd836859 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainsXmlFactory.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultToolchainsXmlFactory.java @@ -57,7 +57,9 @@ public PersistedToolchains read(@Nonnull XmlReaderRequest request) throws XmlRea if (request.getModelId() != null || request.getLocation() != null) { source = new InputSource(request.getLocation()); } - MavenToolchainsStaxReader xml = new MavenToolchainsStaxReader(); + MavenToolchainsStaxReader xml = request.getTransformer() != null + ? new MavenToolchainsStaxReader(request.getTransformer()::transform) + : new MavenToolchainsStaxReader(); xml.setAddDefaultEntities(request.isAddDefaultEntities()); if (reader != null) { return xml.read(reader, request.isStrict(), source); diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java index e4c0d3bdd8a0..5a5735a7922e 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java @@ -26,6 +26,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -1254,7 +1255,7 @@ Model doReadFileModel() throws ModelBuilderException { .path(modelSource.getPath()) .rootDirectory(rootDirectory) .inputStream(is) - .transformer(new InliningTransformer()) + .transformer(new InterningTransformer(session)) .build()); } catch (XmlReaderException e) { if (!strict) { @@ -1267,7 +1268,7 @@ Model doReadFileModel() throws ModelBuilderException { .path(modelSource.getPath()) .rootDirectory(rootDirectory) .inputStream(is) - .transformer(new InliningTransformer()) + .transformer(new InterningTransformer(session)) .build()); } catch (XmlReaderException ne) { // still unreadable even in non-strict mode, rethrow original error @@ -2144,23 +2145,94 @@ public R getRequest() { } } - static class InliningTransformer implements XmlReaderRequest.Transformer { - static final Set CONTEXTS = Set.of( + static class InterningTransformer implements XmlReaderRequest.Transformer { + static final Set DEFAULT_CONTEXTS = Set.of( + // Core Maven coordinates "groupId", "artifactId", "version", "namespaceUri", "packaging", + + // Dependency-related fields "scope", + "type", + "classifier", + + // Build and plugin-related fields "phase", + "goal", + "execution", + + // Repository-related fields "layout", "policy", "checksumPolicy", - "updatePolicy"); + "updatePolicy", + + // Common metadata fields + "modelVersion", + "name", + "url", + "system", + "distribution", + "status", + + // SCM fields + "connection", + "developerConnection", + "tag", + + // Common enum-like values that appear frequently + "id", + "inherited", + "optional"); + + private final Set contexts; + + /** + * Creates an InterningTransformer with default contexts. + */ + InterningTransformer() { + this.contexts = DEFAULT_CONTEXTS; + } + + /** + * Creates an InterningTransformer with contexts from session properties. + * + * @param session the Maven session to read properties from + */ + InterningTransformer(Session session) { + this.contexts = parseContextsFromSession(session); + } + + private Set parseContextsFromSession(Session session) { + String contextsProperty = session.getUserProperties().get(Constants.MAVEN_MODEL_BUILDER_INTERNS); + if (contextsProperty == null) { + contextsProperty = session.getSystemProperties().get(Constants.MAVEN_MODEL_BUILDER_INTERNS); + } + + if (contextsProperty == null || contextsProperty.trim().isEmpty()) { + return DEFAULT_CONTEXTS; + } + + return Arrays.stream(contextsProperty.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toSet()); + } @Override public String transform(String input, String context) { - return CONTEXTS.contains(context) ? input.intern() : input; + return input != null && contexts.contains(context) ? input.intern() : input; + } + + /** + * Get the contexts that will be interned by this transformer. + * Used for testing purposes. + */ + Set getContexts() { + return contexts; } } } diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/XmlFactoryTransformerTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/XmlFactoryTransformerTest.java new file mode 100644 index 000000000000..aa1d3e152219 --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/XmlFactoryTransformerTest.java @@ -0,0 +1,246 @@ +/* + * 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. + */ +package org.apache.maven.impl; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +import org.apache.maven.api.model.Model; +import org.apache.maven.api.plugin.descriptor.PluginDescriptor; +import org.apache.maven.api.services.xml.XmlReaderRequest; +import org.apache.maven.api.settings.Settings; +import org.apache.maven.api.toolchain.PersistedToolchains; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test that all XML factories properly use the transformer from XmlReaderRequest. + */ +class XmlFactoryTransformerTest { + + @Test + void testModelXmlFactoryUsesTransformer() throws Exception { + // Create a test transformer that tracks what contexts are called + List calledContexts = new ArrayList<>(); + XmlReaderRequest.Transformer trackingTransformer = (value, context) -> { + calledContexts.add(context); + return value; + }; + + String pomXml = + """ + + + 4.0.0 + com.example + test-project + 1.0.0 + jar + + """; + + DefaultModelXmlFactory factory = new DefaultModelXmlFactory(); + XmlReaderRequest request = XmlReaderRequest.builder() + .reader(new StringReader(pomXml)) + .transformer(trackingTransformer) + .build(); + + Model model = factory.read(request); + + // Verify the model was parsed correctly + assertEquals("com.example", model.getGroupId()); + assertEquals("test-project", model.getArtifactId()); + assertEquals("1.0.0", model.getVersion()); + assertEquals("jar", model.getPackaging()); + + // Verify that the transformer was called + assertFalse(calledContexts.isEmpty(), "Transformer should have been called"); + assertTrue(calledContexts.contains("groupId"), "groupId context should be called"); + assertTrue(calledContexts.contains("artifactId"), "artifactId context should be called"); + assertTrue(calledContexts.contains("version"), "version context should be called"); + assertTrue(calledContexts.contains("packaging"), "packaging context should be called"); + } + + @Test + void testSettingsXmlFactoryUsesTransformer() throws Exception { + // Create a test transformer that tracks what contexts are called + List calledContexts = new ArrayList<>(); + XmlReaderRequest.Transformer trackingTransformer = (value, context) -> { + calledContexts.add(context); + return value; + }; + + String settingsXml = + """ + + + /path/to/local/repo + + + test-server + testuser + testpass + + + + """; + + DefaultSettingsXmlFactory factory = new DefaultSettingsXmlFactory(); + XmlReaderRequest request = XmlReaderRequest.builder() + .reader(new StringReader(settingsXml)) + .transformer(trackingTransformer) + .build(); + + Settings settings = factory.read(request); + + // Verify the settings were parsed correctly + assertEquals("/path/to/local/repo", settings.getLocalRepository()); + assertEquals(1, settings.getServers().size()); + assertEquals("test-server", settings.getServers().get(0).getId()); + assertEquals("testuser", settings.getServers().get(0).getUsername()); + assertEquals("testpass", settings.getServers().get(0).getPassword()); + + // Verify that the transformer was called + assertFalse(calledContexts.isEmpty(), "Transformer should have been called"); + assertTrue(calledContexts.contains("localRepository"), "localRepository context should be called"); + assertTrue(calledContexts.contains("id"), "id context should be called"); + assertTrue(calledContexts.contains("username"), "username context should be called"); + assertTrue(calledContexts.contains("password"), "password context should be called"); + } + + @Test + void testToolchainsXmlFactoryUsesTransformer() throws Exception { + // Create a test transformer that tracks what contexts are called + List calledContexts = new ArrayList<>(); + XmlReaderRequest.Transformer trackingTransformer = (value, context) -> { + calledContexts.add(context); + return value; + }; + + String toolchainsXml = + """ + + + + jdk + + 17 + openjdk + + + /path/to/jdk17 + + + + """; + + DefaultToolchainsXmlFactory factory = new DefaultToolchainsXmlFactory(); + XmlReaderRequest request = XmlReaderRequest.builder() + .reader(new StringReader(toolchainsXml)) + .transformer(trackingTransformer) + .build(); + + PersistedToolchains toolchains = factory.read(request); + + // Verify the toolchains were parsed correctly + assertEquals(1, toolchains.getToolchains().size()); + assertEquals("jdk", toolchains.getToolchains().get(0).getType()); + assertEquals("17", toolchains.getToolchains().get(0).getProvides().get("version")); + assertEquals("openjdk", toolchains.getToolchains().get(0).getProvides().get("vendor")); + assertEquals( + "/path/to/jdk17", + toolchains + .getToolchains() + .get(0) + .getConfiguration() + .child("jdkHome") + .value()); + + // Verify that the transformer was called + assertFalse(calledContexts.isEmpty(), "Transformer should have been called"); + assertTrue(calledContexts.contains("type"), "type context should be called"); + + // Note: The provides and configuration sections are parsed as Maps/DOM, + // so individual elements like "version", "vendor", "jdkHome" may not + // trigger the transformer directly. The important thing is that the + // transformer is being used by the factory. + } + + @Test + void testPluginXmlFactoryUsesTransformer() throws Exception { + // Create a test transformer that tracks what contexts are called + List calledContexts = new ArrayList<>(); + XmlReaderRequest.Transformer trackingTransformer = (value, context) -> { + calledContexts.add(context); + return value; + }; + + String pluginXml = + """ + + + test-plugin + com.example + test-maven-plugin + 1.0.0 + test + + + compile + compile + com.example.TestMojo + + + + """; + + DefaultPluginXmlFactory factory = new DefaultPluginXmlFactory(); + XmlReaderRequest request = XmlReaderRequest.builder() + .reader(new StringReader(pluginXml)) + .transformer(trackingTransformer) + .build(); + + PluginDescriptor plugin = factory.read(request); + + // Verify the plugin was parsed correctly + assertEquals("test-plugin", plugin.getName()); + assertEquals("com.example", plugin.getGroupId()); + assertEquals("test-maven-plugin", plugin.getArtifactId()); + assertEquals("1.0.0", plugin.getVersion()); + assertEquals("test", plugin.getGoalPrefix()); + assertEquals(1, plugin.getMojos().size()); + assertEquals("compile", plugin.getMojos().get(0).getGoal()); + assertEquals("compile", plugin.getMojos().get(0).getPhase()); + assertEquals("com.example.TestMojo", plugin.getMojos().get(0).getImplementation()); + + // Verify that the transformer was called + assertFalse(calledContexts.isEmpty(), "Transformer should have been called"); + assertTrue(calledContexts.contains("name"), "name context should be called"); + assertTrue(calledContexts.contains("groupId"), "groupId context should be called"); + assertTrue(calledContexts.contains("artifactId"), "artifactId context should be called"); + assertTrue(calledContexts.contains("version"), "version context should be called"); + assertTrue(calledContexts.contains("goal"), "goal context should be called"); + assertTrue(calledContexts.contains("phase"), "phase context should be called"); + assertTrue(calledContexts.contains("implementation"), "implementation context should be called"); + } +} diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/InterningTransformerTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/InterningTransformerTest.java new file mode 100644 index 000000000000..03e8f9018cc4 --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/InterningTransformerTest.java @@ -0,0 +1,413 @@ +/* + * 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. + */ +package org.apache.maven.impl.model; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.maven.api.Constants; +import org.apache.maven.api.Session; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.xml.XmlReaderRequest; +import org.apache.maven.impl.DefaultModelXmlFactory; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * Test class for {@link DefaultModelBuilder.InterningTransformer}. + * Verifies that the transformer correctly interns commonly used string values + * to reduce memory usage during Maven POM parsing. + */ +class InterningTransformerTest { + + @Test + void testTransformerInternsCorrectContexts() { + DefaultModelBuilder.InterningTransformer transformer = new DefaultModelBuilder.InterningTransformer(); + + // Test that contexts in the CONTEXTS set are interned + String groupId1 = transformer.transform("org.apache.maven", "groupId"); + String groupId2 = transformer.transform("org.apache.maven", "groupId"); + assertSame(groupId1, groupId2, "groupId should be interned"); + + String type1 = transformer.transform("jar", "type"); + String type2 = transformer.transform("jar", "type"); + assertSame(type1, type2, "type should be interned"); + + String scope1 = transformer.transform("compile", "scope"); + String scope2 = transformer.transform("compile", "scope"); + assertSame(scope1, scope2, "scope should be interned"); + + String classifier1 = transformer.transform("sources", "classifier"); + String classifier2 = transformer.transform("sources", "classifier"); + assertSame(classifier1, classifier2, "classifier should be interned"); + + String goal1 = transformer.transform("compile", "goal"); + String goal2 = transformer.transform("compile", "goal"); + assertSame(goal1, goal2, "goal should be interned"); + + String modelVersion1 = transformer.transform("4.0.0", "modelVersion"); + String modelVersion2 = transformer.transform("4.0.0", "modelVersion"); + assertSame(modelVersion1, modelVersion2, "modelVersion should be interned"); + + // Test that contexts not in the CONTEXTS set are not interned + // Use new String() to avoid automatic interning by JVM + String value1 = new String("some-value"); + String value2 = new String("some-value"); + String nonInterned1 = transformer.transform(value1, "nonInterned"); + String nonInterned2 = transformer.transform(value2, "nonInterned"); + assertSame(value1, nonInterned1, "non-interned context should return same instance"); + assertSame(value2, nonInterned2, "non-interned context should return same instance"); + assertNotSame(nonInterned1, nonInterned2, "different input instances should remain different"); + assertEquals(nonInterned1, nonInterned2, "but values should still be equal"); + } + + @Test + void testTransformerContainsExpectedContexts() { + // Verify that the DEFAULT_CONTEXTS set contains all the expected fields + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("groupId")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("artifactId")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("version")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("packaging")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("scope")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("type")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("classifier")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("goal")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("execution")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("phase")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("modelVersion")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("name")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("url")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("system")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("distribution")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("status")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("connection")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("developerConnection")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("tag")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("id")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("inherited")); + assertTrue(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("optional")); + + // Verify that non-interned contexts are not in the set + assertFalse(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("nonInterned")); + assertFalse(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("description")); + assertFalse(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS.contains("randomField")); + } + + @Test + void testTransformerWithNullAndEmptyValues() { + DefaultModelBuilder.InterningTransformer transformer = new DefaultModelBuilder.InterningTransformer(); + + // Test with null value + String result1 = transformer.transform(null, "groupId"); + String result2 = transformer.transform(null, "groupId"); + assertNull(result1); + assertNull(result2); + + // Test with empty string + String empty1 = transformer.transform("", "artifactId"); + String empty2 = transformer.transform("", "artifactId"); + assertSame(empty1, empty2, "empty strings should be interned"); + + // Test with whitespace + String whitespace1 = transformer.transform(" ", "version"); + String whitespace2 = transformer.transform(" ", "version"); + assertSame(whitespace1, whitespace2, "whitespace strings should be interned"); + } + + @Test + void testTransformerIsUsedDuringPomParsing() throws Exception { + // Create a test transformer that tracks what contexts are called + List calledContexts = new ArrayList<>(); + XmlReaderRequest.Transformer trackingTransformer = (value, context) -> { + calledContexts.add(context); + return value; + }; + + String pomXml = + """ + + + 4.0.0 + com.example + test-project + 1.0.0 + jar + + + + org.apache.maven + maven-core + 3.9.0 + compile + jar + sources + + + + """; + + DefaultModelXmlFactory factory = new DefaultModelXmlFactory(); + XmlReaderRequest request = XmlReaderRequest.builder() + .reader(new StringReader(pomXml)) + .transformer(trackingTransformer) + .build(); + + Model model = factory.read(request); + + // Verify the model was parsed correctly + assertEquals("com.example", model.getGroupId()); + assertEquals("test-project", model.getArtifactId()); + assertEquals("1.0.0", model.getVersion()); + assertEquals("jar", model.getPackaging()); + + // Verify that the transformer was called for the expected contexts + assertTrue(calledContexts.contains("groupId"), "groupId context should be called"); + assertTrue(calledContexts.contains("artifactId"), "artifactId context should be called"); + assertTrue(calledContexts.contains("version"), "version context should be called"); + assertTrue(calledContexts.contains("packaging"), "packaging context should be called"); + assertTrue(calledContexts.contains("scope"), "scope context should be called"); + assertTrue(calledContexts.contains("type"), "type context should be called"); + assertTrue(calledContexts.contains("classifier"), "classifier context should be called"); + + // Verify specific paths are called correctly + long groupIdCount = calledContexts.stream().filter("groupId"::equals).count(); + assertTrue(groupIdCount >= 2, "groupId should be called at least 2 times (project, dependency)"); + } + + @Test + void testInterningTransformerWithRealPomParsing() throws Exception { + String pomXml = + """ + + + 4.0.0 + org.apache.maven + maven-core + 4.0.0 + jar + + + + org.apache.maven + maven-api + 4.0.0 + compile + + + + """; + + DefaultModelXmlFactory factory = new DefaultModelXmlFactory(); + DefaultModelBuilder.InterningTransformer transformer = new DefaultModelBuilder.InterningTransformer(); + + XmlReaderRequest request = XmlReaderRequest.builder() + .reader(new StringReader(pomXml)) + .transformer(transformer) + .build(); + + Model model = factory.read(request); + + // Verify the model was parsed correctly + assertEquals("org.apache.maven", model.getGroupId()); + assertEquals("maven-core", model.getArtifactId()); + assertEquals("4.0.0", model.getVersion()); + assertEquals("jar", model.getPackaging()); + + // Verify dependency was parsed + assertEquals(1, model.getDependencies().size()); + assertEquals("org.apache.maven", model.getDependencies().get(0).getGroupId()); + assertEquals("maven-api", model.getDependencies().get(0).getArtifactId()); + assertEquals("4.0.0", model.getDependencies().get(0).getVersion()); + assertEquals("compile", model.getDependencies().get(0).getScope()); + } + + @Test + void testTransformerWithSessionPropertyUserProperties() { + // Test with custom contexts from user properties + Map userProperties = new HashMap<>(); + userProperties.put(Constants.MAVEN_MODEL_BUILDER_INTERNS, "groupId,artifactId,customField"); + + Session session = Mockito.mock(Session.class); + when(session.getUserProperties()).thenReturn(userProperties); + + DefaultModelBuilder.InterningTransformer transformer = new DefaultModelBuilder.InterningTransformer(session); + + // Test that custom contexts are used + assertTrue(transformer.getContexts().contains("groupId")); + assertTrue(transformer.getContexts().contains("artifactId")); + assertTrue(transformer.getContexts().contains("customField")); + + // Test that default contexts not in the custom list are not used + assertFalse(transformer.getContexts().contains("version")); + assertFalse(transformer.getContexts().contains("scope")); + + // Test interning behavior + String groupId1 = transformer.transform("org.apache.maven", "groupId"); + String groupId2 = transformer.transform("org.apache.maven", "groupId"); + assertSame(groupId1, groupId2, "groupId should be interned"); + + String custom1 = transformer.transform("test-value", "customField"); + String custom2 = transformer.transform("test-value", "customField"); + assertSame(custom1, custom2, "customField should be interned"); + + // Test that non-custom contexts are not interned + String version1 = new String("1.0.0"); + String version2 = new String("1.0.0"); + String nonInterned1 = transformer.transform(version1, "version"); + String nonInterned2 = transformer.transform(version2, "version"); + assertSame(version1, nonInterned1, "version should not be interned"); + assertSame(version2, nonInterned2, "version should not be interned"); + assertNotSame(nonInterned1, nonInterned2, "different input instances should remain different"); + } + + @Test + void testTransformerWithSessionPropertySystemProperties() { + // Test with custom contexts from system properties + Map systemProperties = new HashMap<>(); + systemProperties.put(Constants.MAVEN_MODEL_BUILDER_INTERNS, "scope,type"); + + Session session = Mockito.mock(Session.class); + when(session.getSystemProperties()).thenReturn(systemProperties); + + DefaultModelBuilder.InterningTransformer transformer = new DefaultModelBuilder.InterningTransformer(session); + + // Test that custom contexts are used + assertTrue(transformer.getContexts().contains("scope")); + assertTrue(transformer.getContexts().contains("type")); + assertEquals(2, transformer.getContexts().size()); + + // Test interning behavior + String scope1 = transformer.transform("compile", "scope"); + String scope2 = transformer.transform("compile", "scope"); + assertSame(scope1, scope2, "scope should be interned"); + } + + @Test + void testTransformerUserPropertiesOverrideSystemProperties() { + // Test that user properties take precedence over system properties + Map systemProperties = new HashMap<>(); + systemProperties.put(Constants.MAVEN_MODEL_BUILDER_INTERNS, "scope,type"); + + Map userProperties = new HashMap<>(); + userProperties.put(Constants.MAVEN_MODEL_BUILDER_INTERNS, "groupId,artifactId"); + + Session session = Mockito.mock(Session.class); + when(session.getUserProperties()).thenReturn(userProperties); + when(session.getSystemProperties()).thenReturn(systemProperties); + + DefaultModelBuilder.InterningTransformer transformer = new DefaultModelBuilder.InterningTransformer(session); + + // Test that user properties are used, not system properties + assertTrue(transformer.getContexts().contains("groupId")); + assertTrue(transformer.getContexts().contains("artifactId")); + assertFalse(transformer.getContexts().contains("scope")); + assertFalse(transformer.getContexts().contains("type")); + assertEquals(2, transformer.getContexts().size()); + } + + @Test + void testTransformerWithEmptySessionProperty() { + // Test with empty property value - should use defaults + Map userProperties = new HashMap<>(); + userProperties.put(Constants.MAVEN_MODEL_BUILDER_INTERNS, ""); + + Session session = Mockito.mock(Session.class); + when(session.getUserProperties()).thenReturn(userProperties); + + DefaultModelBuilder.InterningTransformer transformer = new DefaultModelBuilder.InterningTransformer(session); + + // Should use default contexts + assertEquals(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS, transformer.getContexts()); + } + + @Test + void testTransformerWithWhitespaceOnlySessionProperty() { + // Test with whitespace-only property value - should use defaults + Map userProperties = new HashMap<>(); + userProperties.put(Constants.MAVEN_MODEL_BUILDER_INTERNS, " "); + + Session session = Mockito.mock(Session.class); + when(session.getUserProperties()).thenReturn(userProperties); + + DefaultModelBuilder.InterningTransformer transformer = new DefaultModelBuilder.InterningTransformer(session); + + // Should use default contexts + assertEquals(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS, transformer.getContexts()); + } + + @Test + void testTransformerWithNoSessionProperty() { + // Test with no property set - should use defaults + Session session = Mockito.mock(Session.class); + + DefaultModelBuilder.InterningTransformer transformer = new DefaultModelBuilder.InterningTransformer(session); + + // Should use default contexts + assertEquals(DefaultModelBuilder.InterningTransformer.DEFAULT_CONTEXTS, transformer.getContexts()); + } + + @Test + void testTransformerWithCommaSeparatedValues() { + // Test parsing of comma-separated values with various whitespace + Map userProperties = new HashMap<>(); + userProperties.put(Constants.MAVEN_MODEL_BUILDER_INTERNS, "groupId, artifactId , version, scope ,type"); + + Session session = Mockito.mock(Session.class); + when(session.getUserProperties()).thenReturn(userProperties); + + DefaultModelBuilder.InterningTransformer transformer = new DefaultModelBuilder.InterningTransformer(session); + + // Test that all values are parsed correctly (whitespace trimmed) + assertTrue(transformer.getContexts().contains("groupId")); + assertTrue(transformer.getContexts().contains("artifactId")); + assertTrue(transformer.getContexts().contains("version")); + assertTrue(transformer.getContexts().contains("scope")); + assertTrue(transformer.getContexts().contains("type")); + assertEquals(5, transformer.getContexts().size()); + } + + @Test + void testTransformerWithEmptyCommaSeparatedValues() { + // Test parsing with empty values in comma-separated list + Map userProperties = new HashMap<>(); + userProperties.put(Constants.MAVEN_MODEL_BUILDER_INTERNS, "groupId,,artifactId, ,version"); + + Session session = Mockito.mock(Session.class); + when(session.getUserProperties()).thenReturn(userProperties); + + DefaultModelBuilder.InterningTransformer transformer = new DefaultModelBuilder.InterningTransformer(session); + + // Test that empty values are filtered out + assertTrue(transformer.getContexts().contains("groupId")); + assertTrue(transformer.getContexts().contains("artifactId")); + assertTrue(transformer.getContexts().contains("version")); + assertEquals(3, transformer.getContexts().size()); + } +} diff --git a/src/site/markdown/configuration.properties b/src/site/markdown/configuration.properties index 859836cc4f4d..4cb611425f45 100644 --- a/src/site/markdown/configuration.properties +++ b/src/site/markdown/configuration.properties @@ -20,7 +20,7 @@ # Generated from: maven-resolver-tools/src/main/resources/configuration.properties.vm # To modify this file, edit the template and regenerate. # -props.count = 65 +props.count = 66 props.1.key = maven.build.timestamp.format props.1.configurationType = String props.1.description = Build timestamp format. @@ -164,7 +164,13 @@ props.24.description = User property for controlling "maven personality". If act props.24.defaultValue = false props.24.since = 4.0.0 props.24.configurationSource = User properties -props.25.key = maven.modelBuilder.parallelism +props.25.key = maven.modelBuilder.interns +props.25.configurationType = String +props.25.description = Comma-separated list of XML contexts/fields to intern during POM parsing for memory optimization. When not specified, a default set of commonly repeated contexts will be used. Example: "groupId,artifactId,version,scope,type" +props.25.defaultValue = +props.25.since = 4.0.0 +props.25.configurationSource = User properties +props.26.key = maven.modelBuilder.parallelism props.25.configurationType = Integer props.25.description = ProjectBuilder parallelism. props.25.defaultValue = cores/2 + 1 @@ -397,6 +403,7 @@ props.63.description = Maven snapshot: contains "true" if this Maven is a snapsh props.63.defaultValue = props.63.since = 4.0.0 props.63.configurationSource = system_properties +<<<<<<< HEAD props.64.key = maven.versionRangeResolver.natureOverride props.64.configurationType = String props.64.description = Configuration property for version range resolution used metadata "nature". It may contain following string values:
  • "auto" - decision done based on range being resolver: if any boundary is snapshot, use "release_or_snapshot", otherwise "release"
  • "release_or_snapshot" - the default
  • "release" - query only release repositories to discover versions
  • "snapshot" - query only snapshot repositories to discover versions
Default (when unset) is existing Maven behaviour: "release_or_snapshots". @@ -409,3 +416,11 @@ props.65.description = User property for disabling version resolver cache. props.65.defaultValue = false props.65.since = 3.0.0 props.65.configurationSource = User properties +======= +props.64.key = maven.versionResolver.noCache +props.64.configurationType = Boolean +props.64.description = User property for disabling version resolver cache. +props.64.defaultValue = false +props.64.since = 3.0.0 +props.64.configurationSource = User properties +>>>>>>> 4515e4e39b (Expand value interning optimization and add configurable session property) diff --git a/src/site/markdown/configuration.yaml b/src/site/markdown/configuration.yaml index c9f60fb60cae..20935442b805 100644 --- a/src/site/markdown/configuration.yaml +++ b/src/site/markdown/configuration.yaml @@ -164,6 +164,12 @@ props: defaultValue: false since: 4.0.0 configurationSource: User properties + - key: maven.modelBuilder.interns + configurationType: String + description: "Comma-separated list of XML contexts/fields to intern during POM parsing for memory optimization. When not specified, a default set of commonly repeated contexts will be used. Example: \"groupId,artifactId,version,scope,type\"" + defaultValue: + since: 4.0.0 + configurationSource: User properties - key: maven.modelBuilder.parallelism configurationType: Integer description: "ProjectBuilder parallelism." diff --git a/src/site/markdown/maven-configuration.md b/src/site/markdown/maven-configuration.md index 29fff1f78248..a98d449cd9b3 100644 --- a/src/site/markdown/maven-configuration.md +++ b/src/site/markdown/maven-configuration.md @@ -55,6 +55,7 @@ To modify this file, edit the template and regenerate. | `maven.logger.showThreadName` | `Boolean` | Set to true if you want to output the current thread name. Defaults to true. | `true` | 4.0.0 | User properties | | `maven.logger.warnLevelString` | `String` | The string value output for the warn level. Defaults to WARN. | `WARN` | 4.0.0 | User properties | | `maven.maven3Personality` | `Boolean` | User property for controlling "maven personality". If activated Maven will behave as previous major version, Maven 3. | `false` | 4.0.0 | User properties | +| `maven.modelBuilder.interns` | `String` | Comma-separated list of XML contexts/fields to intern during POM parsing for memory optimization. When not specified, a default set of commonly repeated contexts will be used. Example: "groupId,artifactId,version,scope,type" | - | 4.0.0 | User properties | | `maven.modelBuilder.parallelism` | `Integer` | ProjectBuilder parallelism. | `cores/2 + 1` | 4.0.0 | User properties | | `maven.plugin.validation` | `String` | Plugin validation level. | `inline` | 3.9.2 | User properties | | `maven.plugin.validation.excludes` | `String` | Plugin validation exclusions. | - | 3.9.6 | User properties |