diff --git a/maven-core/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java b/maven-core/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java index 7983388bd28..beeee1944e0 100644 --- a/maven-core/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java +++ b/maven-core/src/main/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilder.java @@ -19,24 +19,30 @@ * under the License. */ -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - import org.apache.maven.building.Problem; import org.apache.maven.building.ProblemCollector; import org.apache.maven.building.ProblemCollectorFactory; import org.apache.maven.building.Source; import org.apache.maven.toolchain.io.ToolchainsParseException; import org.apache.maven.toolchain.io.ToolchainsReader; +import org.apache.maven.toolchain.io.ToolchainsWriter; import org.apache.maven.toolchain.merge.MavenToolchainMerger; import org.apache.maven.toolchain.model.PersistedToolchains; import org.apache.maven.toolchain.model.TrackableBase; +import org.codehaus.plexus.interpolation.EnvarBasedValueSource; +import org.codehaus.plexus.interpolation.InterpolationException; +import org.codehaus.plexus.interpolation.InterpolationPostProcessor; +import org.codehaus.plexus.interpolation.RegexBasedInterpolator; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Collections; +import java.util.List; +import java.util.Map; /** * @@ -49,7 +55,10 @@ public class DefaultToolchainsBuilder implements ToolchainsBuilder { private MavenToolchainMerger toolchainsMerger = new MavenToolchainMerger(); - + + @Inject + private ToolchainsWriter toolchainsWriter; + @Inject private ToolchainsReader toolchainsReader; @@ -66,16 +75,86 @@ public ToolchainsBuildingResult build( ToolchainsBuildingRequest request ) toolchainsMerger.merge( userToolchains, globalToolchains, TrackableBase.GLOBAL_LEVEL ); problems.setSource( "" ); - + + userToolchains = interpolate( userToolchains, problems ); + if ( hasErrors( problems.getProblems() ) ) { throw new ToolchainsBuildingException( problems.getProblems() ); } - - + + return new DefaultToolchainsBuildingResult( userToolchains, problems.getProblems() ); } + private PersistedToolchains interpolate( PersistedToolchains toolchains, ProblemCollector problems ) + { + + StringWriter stringWriter = new StringWriter( 1024 * 4 ); + try + { + toolchainsWriter.write( stringWriter, null, toolchains ); + } + catch ( IOException e ) + { + throw new IllegalStateException( "Failed to serialize toolchains to memory", e ); + } + + String serializedToolchains = stringWriter.toString(); + + RegexBasedInterpolator interpolator = new RegexBasedInterpolator(); + + try + { + interpolator.addValueSource( new EnvarBasedValueSource() ); + } + catch ( IOException e ) + { + problems.add( Problem.Severity.WARNING, "Failed to use environment variables for interpolation: " + + e.getMessage(), -1, -1, e ); + } + + interpolator.addPostProcessor( new InterpolationPostProcessor() + { + @Override + public Object execute( String expression, Object value ) + { + if ( value != null ) + { + // we're going to parse this back in as XML so we need to escape XML markup + value = value.toString().replace( "&", "&" ).replace( "<", "<" ).replace( ">", ">" ); + return value; + } + return null; + } + } ); + + try + { + serializedToolchains = interpolator.interpolate( serializedToolchains ); + } + catch ( InterpolationException e ) + { + problems.add( Problem.Severity.ERROR, "Failed to interpolate toolchains: " + e.getMessage(), -1, -1, e ); + return toolchains; + } + + PersistedToolchains result; + try + { + Map options = Collections.singletonMap( ToolchainsReader.IS_STRICT, Boolean.FALSE ); + + result = toolchainsReader.read( new StringReader( serializedToolchains ), options ); + } + catch ( IOException e ) + { + problems.add( Problem.Severity.ERROR, "Failed to interpolate toolchains: " + e.getMessage(), -1, -1, e ); + return toolchains; + } + + return result; + } + private PersistedToolchains readToolchains( Source toolchainsSource, ToolchainsBuildingRequest request, ProblemCollector problems ) { diff --git a/maven-core/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsWriter.java b/maven-core/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsWriter.java new file mode 100644 index 00000000000..a533766a2bd --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/toolchain/io/DefaultToolchainsWriter.java @@ -0,0 +1,54 @@ +package org.apache.maven.toolchain.io; + +/* + * 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. + */ + +import org.apache.maven.toolchain.model.PersistedToolchains; +import org.apache.maven.toolchain.model.io.xpp3.MavenToolchainsXpp3Writer; + +import javax.inject.Named; +import javax.inject.Singleton; +import java.io.IOException; +import java.io.Writer; +import java.util.Map; +import java.util.Objects; + +/** + * Handles serialization of toolchains into the default textual format. + * + * @author Mike Mol + * @author Martin Kanters + */ +@Named +@Singleton +public class DefaultToolchainsWriter implements ToolchainsWriter +{ + + @Override + public void write( Writer output, Map options, PersistedToolchains toolchains ) throws IOException + { + Objects.requireNonNull( output, "output cannot be null" ); + Objects.requireNonNull( toolchains, "toolchains cannot be null" ); + + try ( final Writer out = output ) + { + new MavenToolchainsXpp3Writer().write( out, toolchains ); + } + } +} diff --git a/maven-core/src/main/java/org/apache/maven/toolchain/io/ToolchainsWriter.java b/maven-core/src/main/java/org/apache/maven/toolchain/io/ToolchainsWriter.java new file mode 100644 index 00000000000..0b15f3451ad --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/toolchain/io/ToolchainsWriter.java @@ -0,0 +1,48 @@ +package org.apache.maven.toolchain.io; + +/* + * 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. + */ + +import org.apache.maven.toolchain.model.PersistedToolchains; + +import java.io.IOException; +import java.io.Writer; +import java.util.Map; + +/** + * Handles serialization of toolchains into some kind of textual format like XML. + * + * @author Mike Mol + * @author Martin Kanters + */ +public interface ToolchainsWriter +{ + + /** + * Writes the supplied toolchains to the specified character writer. The writer will be automatically closed before + * the method returns. + * + * @param output The writer to serialize the toolchains to, must not be {@code null}. + * @param options The options to use for serialization, may be {@code null} to use the default values. + * @param toolchains The toolchains to serialize, must not be {@code null}. + * @throws IOException If the toolchains could not be serialized. + */ + void write( Writer output, Map options, PersistedToolchains toolchains ) + throws IOException; +} diff --git a/maven-core/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java b/maven-core/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java index fc530df5a38..2c20d3c21fb 100644 --- a/maven-core/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java +++ b/maven-core/src/test/java/org/apache/maven/toolchain/building/DefaultToolchainsBuilderTest.java @@ -19,40 +19,54 @@ * under the License. */ -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; - import org.apache.maven.building.StringSource; +import org.apache.maven.toolchain.io.DefaultToolchainsReader; +import org.apache.maven.toolchain.io.DefaultToolchainsWriter; import org.apache.maven.toolchain.io.ToolchainsParseException; -import org.apache.maven.toolchain.io.ToolchainsReader; import org.apache.maven.toolchain.model.PersistedToolchains; import org.apache.maven.toolchain.model.ToolchainModel; +import org.codehaus.plexus.interpolation.os.OperatingSystemUtils; +import org.codehaus.plexus.util.xml.Xpp3Dom; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentMatchers; import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; public class DefaultToolchainsBuilderTest { private static final String LS = System.getProperty( "line.separator" ); - - @Mock - private ToolchainsReader toolchainsReader; + + @Spy + private DefaultToolchainsReader toolchainsReader; + + @Spy + private DefaultToolchainsWriter toolchainsWriter; @InjectMocks - private DefaultToolchainsBuilder toolchainBuilder = new DefaultToolchainsBuilder(); + private DefaultToolchainsBuilder toolchainBuilder; @Before public void onSetup() { MockitoAnnotations.initMocks( this ); + + Map envVarMap = new HashMap<>(); + envVarMap.put("testKey", "testValue"); + envVarMap.put("testSpecialCharactersKey", ""); + OperatingSystemUtils.setEnvVarSource(new TestEnvVarSource(envVarMap)); } @Test @@ -78,7 +92,7 @@ public void testBuildRequestWithUserToolchains() toolchain.setType( "TYPE" ); toolchain.addProvide( "key", "user_value" ); userResult.addToolchain( toolchain ); - when( toolchainsReader.read( any( InputStream.class ), ArgumentMatchers.anyMap()) ).thenReturn( userResult ); + doReturn(userResult).when( toolchainsReader ).read( any( InputStream.class ), ArgumentMatchers.anyMap()); ToolchainsBuildingResult result = toolchainBuilder.build( request ); assertNotNull( result.getEffectiveToolchains() ); @@ -101,7 +115,7 @@ public void testBuildRequestWithGlobalToolchains() toolchain.setType( "TYPE" ); toolchain.addProvide( "key", "global_value" ); globalResult.addToolchain( toolchain ); - when( toolchainsReader.read( any( InputStream.class ), ArgumentMatchers.anyMap()) ).thenReturn( globalResult ); + doReturn(globalResult).when( toolchainsReader ).read( any( InputStream.class ), ArgumentMatchers.anyMap()); ToolchainsBuildingResult result = toolchainBuilder.build( request ); assertNotNull( result.getEffectiveToolchains() ); @@ -131,7 +145,7 @@ public void testBuildRequestWithBothToolchains() globalToolchain.setType( "TYPE" ); globalToolchain.addProvide( "key", "global_value" ); globalResult.addToolchain( globalToolchain ); - when( toolchainsReader.read( any( InputStream.class ), ArgumentMatchers.anyMap()) ).thenReturn( globalResult ).thenReturn( userResult ); + doReturn(globalResult).doReturn(userResult).when( toolchainsReader ).read( any( InputStream.class ), ArgumentMatchers.anyMap()); ToolchainsBuildingResult result = toolchainBuilder.build( request ); assertNotNull( result.getEffectiveToolchains() ); @@ -143,43 +157,127 @@ public void testBuildRequestWithBothToolchains() assertNotNull( result.getProblems() ); assertEquals( 0, result.getProblems().size() ); } - + @Test public void testStrictToolchainsParseException() throws Exception { ToolchainsBuildingRequest request = new DefaultToolchainsBuildingRequest(); request.setGlobalToolchainsSource( new StringSource( "" ) ); ToolchainsParseException parseException = new ToolchainsParseException( "MESSAGE", 4, 2 ); - when( toolchainsReader.read( any( InputStream.class ), ArgumentMatchers.anyMap()) ).thenThrow( parseException ); - + doThrow(parseException).when( toolchainsReader ).read( any( InputStream.class ), ArgumentMatchers.anyMap()); + try { toolchainBuilder.build( request ); } catch ( ToolchainsBuildingException e ) { - assertEquals( "1 problem was encountered while building the effective toolchains" + LS + + assertEquals( "1 problem was encountered while building the effective toolchains" + LS + "[FATAL] Non-parseable toolchains (memory): MESSAGE @ line 4, column 2" + LS, e.getMessage() ); } } - + @Test public void testIOException() throws Exception { ToolchainsBuildingRequest request = new DefaultToolchainsBuildingRequest(); request.setGlobalToolchainsSource( new StringSource( "", "LOCATION" ) ); IOException ioException = new IOException( "MESSAGE" ); - when( toolchainsReader.read( any( InputStream.class ), ArgumentMatchers.anyMap()) ).thenThrow( ioException ); - + doThrow(ioException).when( toolchainsReader ).read( any( InputStream.class ), ArgumentMatchers.anyMap()); + try { toolchainBuilder.build( request ); } catch ( ToolchainsBuildingException e ) { - assertEquals( "1 problem was encountered while building the effective toolchains" + LS + + assertEquals( "1 problem was encountered while building the effective toolchains" + LS + "[FATAL] Non-readable toolchains LOCATION: MESSAGE" + LS, e.getMessage() ); } } - + + @Test + public void testEnvironmentVariablesAreInterpolated() + throws Exception + { + ToolchainsBuildingRequest request = new DefaultToolchainsBuildingRequest(); + request.setUserToolchainsSource( new StringSource( "" ) ); + + PersistedToolchains persistedToolchains = new PersistedToolchains(); + ToolchainModel toolchain = new ToolchainModel(); + toolchain.setType( "TYPE" ); + toolchain.addProvide( "key", "${env.testKey}" ); + + Xpp3Dom configurationChild = new Xpp3Dom("jdkHome"); + configurationChild.setValue("${env.testKey}"); + Xpp3Dom configuration = new Xpp3Dom("configuration"); + configuration.addChild(configurationChild); + toolchain.setConfiguration(configuration); + persistedToolchains.addToolchain( toolchain ); + doReturn(persistedToolchains).when( toolchainsReader ).read( any( InputStream.class ), ArgumentMatchers.anyMap()); + + ToolchainsBuildingResult result = toolchainBuilder.build( request ); + String interpolatedValue = "testValue"; + assertEquals(interpolatedValue, result.getEffectiveToolchains().getToolchains().get(0).getProvides().getProperty( "key" ) ); + Xpp3Dom toolchainConfiguration = (Xpp3Dom) result.getEffectiveToolchains().getToolchains().get(0).getConfiguration(); + assertEquals(interpolatedValue, toolchainConfiguration.getChild("jdkHome").getValue()); + assertNotNull( result.getProblems() ); + assertEquals( 0, result.getProblems().size() ); + } + + @Test + public void testNonExistingEnvironmentVariablesAreNotInterpolated() + throws Exception + { + ToolchainsBuildingRequest request = new DefaultToolchainsBuildingRequest(); + request.setUserToolchainsSource( new StringSource( "" ) ); + + PersistedToolchains persistedToolchains = new PersistedToolchains(); + ToolchainModel toolchain = new ToolchainModel(); + toolchain.setType( "TYPE" ); + toolchain.addProvide( "key", "${env.testNonExistingKey}" ); + + persistedToolchains.addToolchain( toolchain ); + doReturn(persistedToolchains).when( toolchainsReader ).read( any( InputStream.class ), ArgumentMatchers.anyMap()); + + ToolchainsBuildingResult result = toolchainBuilder.build( request ); + assertEquals("${env.testNonExistingKey}", result.getEffectiveToolchains().getToolchains().get(0).getProvides().getProperty( "key" ) ); + assertNotNull( result.getProblems() ); + assertEquals( 0, result.getProblems().size() ); + } + + @Test + public void testEnvironmentVariablesWithSpecialCharactersAreInterpolated() + throws Exception + { + ToolchainsBuildingRequest request = new DefaultToolchainsBuildingRequest(); + request.setUserToolchainsSource( new StringSource( "" ) ); + + PersistedToolchains persistedToolchains = new PersistedToolchains(); + ToolchainModel toolchain = new ToolchainModel(); + toolchain.setType( "TYPE" ); + toolchain.addProvide( "key", "${env.testSpecialCharactersKey}" ); + + persistedToolchains.addToolchain( toolchain ); + doReturn(persistedToolchains).when( toolchainsReader ).read( any( InputStream.class ), ArgumentMatchers.anyMap()); + + ToolchainsBuildingResult result = toolchainBuilder.build( request ); + String interpolatedValue = ""; + assertEquals(interpolatedValue, result.getEffectiveToolchains().getToolchains().get(0).getProvides().getProperty( "key" ) ); + assertNotNull( result.getProblems() ); + assertEquals( 0, result.getProblems().size() ); + } + + static class TestEnvVarSource implements OperatingSystemUtils.EnvVarSource { + private final Map envVarMap; + + TestEnvVarSource(Map envVarMap) { + this.envVarMap = envVarMap; + } + + public Map getEnvMap() { + return envVarMap; + } + } + } diff --git a/pom.xml b/pom.xml index cb9dae5b664..e734a1d6376 100644 --- a/pom.xml +++ b/pom.xml @@ -150,6 +150,12 @@ under the License. Fabiano Cipriano de Oliveira (MNG-6261) + + Mike Mol (MNG-6665) + + + Martin Kanters (MNG-6665) +