From 0d9cc56f01f0c312ed8042cbe276df3b4ff39392 Mon Sep 17 00:00:00 2001 From: Romain Manni-Bucau Date: Fri, 17 Jul 2020 12:08:19 +0200 Subject: [PATCH] Fixes #160, ensure the java classloader is a child first one and supports to excludes some gathered classpath element to solve manually conflicts --- pom.xml | 52 ++ .../org/codehaus/mojo/exec/ExecJavaMojo.java | 33 +- .../org/codehaus/mojo/exec/LoaderFinder.java | 34 -- .../mojo/exec/URLClassLoaderBuilder.java | 497 +++++++++++++++++- .../codehaus/mojo/exec/ExecJavaMojoTest.java | 51 +- .../org/codehaus/mojo/exec/Slf4jMain.java | 25 + .../mojo/exec/URLClassLoaderBuilderTest.java | 67 +++ src/test/projects/project16/pom.xml | 48 ++ src/test/projects/project17/pom.xml | 43 ++ 9 files changed, 809 insertions(+), 41 deletions(-) delete mode 100644 src/main/java/org/codehaus/mojo/exec/LoaderFinder.java create mode 100644 src/test/java/org/codehaus/mojo/exec/Slf4jMain.java create mode 100644 src/test/java/org/codehaus/mojo/exec/URLClassLoaderBuilderTest.java create mode 100644 src/test/projects/project16/pom.xml create mode 100644 src/test/projects/project17/pom.xml diff --git a/pom.xml b/pom.xml index bdb56539..7a562ea6 100644 --- a/pom.xml +++ b/pom.xml @@ -184,10 +184,17 @@ 1.9.0 test + + org.slf4j + slf4j-simple + ${slf4j.version} + 3.0 + + 1.7.30 @@ -269,6 +276,51 @@ + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.2 + + + copy-test-deps + generate-test-resources + + copy + + + + + org.slf4j + slf4j-api + ${slf4j.version} + jar + false + slf4j-api.jar + + + org.slf4j + slf4j-simple + ${slf4j.version} + jar + false + slf4j-simple.jar + + + org.slf4j + slf4j-jdk14 + ${slf4j.version} + jar + false + slf4j-jdk14.jar + + + ${project.build.directory}/test-dependencies/ + false + true + + + + diff --git a/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java b/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java index fe77c1ec..7b476846 100644 --- a/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java +++ b/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java @@ -5,6 +5,7 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.InvocationTargetException; +import java.net.URLClassLoader; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -183,6 +184,15 @@ public class ExecJavaMojo @Parameter private List additionalClasspathElements; + /** + * List of file to exclude from the classpath. + * It matches the jar name, for example {@code slf4j-simple-1.7.30.jar}. + * + * @since 3.0.1 + */ + @Parameter + private List classpathFilenameExclusions; + /** * Execute goal. * @@ -270,7 +280,8 @@ public void run() } } }, mainClass + ".main()" ); - bootstrapThread.setContextClassLoader( getClassLoader() ); + URLClassLoader classLoader = getClassLoader(); + bootstrapThread.setContextClassLoader( classLoader ); setSystemProperties(); bootstrapThread.start(); @@ -298,6 +309,18 @@ public void run() } } + if ( classLoader != null ) + { + try + { + classLoader.close(); + } + catch ( IOException e ) + { + getLog().error(e.getMessage(), e); + } + } + if ( originalSystemProperties != null ) { System.setProperties( originalSystemProperties ); @@ -485,7 +508,7 @@ private void setSystemProperties() * @return the classloader * @throws MojoExecutionException if a problem happens */ - private ClassLoader getClassLoader() + private URLClassLoader getClassLoader() throws MojoExecutionException { List classpathURLs = new ArrayList<>(); @@ -495,7 +518,11 @@ private ClassLoader getClassLoader() try { - return LoaderFinder.find( classpathURLs, mainClass ); + return URLClassLoaderBuilder.builder() + .setLogger( getLog() ) + .setPaths( classpathURLs ) + .setExclusions( classpathFilenameExclusions ) + .build(); } catch ( NullPointerException | IOException e ) { diff --git a/src/main/java/org/codehaus/mojo/exec/LoaderFinder.java b/src/main/java/org/codehaus/mojo/exec/LoaderFinder.java deleted file mode 100644 index 390df4ec..00000000 --- a/src/main/java/org/codehaus/mojo/exec/LoaderFinder.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.codehaus.mojo.exec; - -/* - * 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 java.io.IOException; -import java.nio.file.Path; -import java.util.List; - -class LoaderFinder -{ - static ClassLoader find( List dependencies, String mainClass ) - throws IOException - { - return URLClassLoaderBuilder.builder().setPaths( dependencies ).build(); - } - -} diff --git a/src/main/java/org/codehaus/mojo/exec/URLClassLoaderBuilder.java b/src/main/java/org/codehaus/mojo/exec/URLClassLoaderBuilder.java index 77286521..95762e29 100644 --- a/src/main/java/org/codehaus/mojo/exec/URLClassLoaderBuilder.java +++ b/src/main/java/org/codehaus/mojo/exec/URLClassLoaderBuilder.java @@ -19,15 +19,24 @@ * under the License. */ +import org.apache.maven.plugin.logging.Log; + +import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; import java.util.List; +import static java.util.Arrays.asList; + /** * * @author Robert Scholte @@ -35,7 +44,9 @@ */ class URLClassLoaderBuilder { + private Log logger; private Collection paths; + private Collection exclusions; private URLClassLoaderBuilder() { @@ -46,18 +57,38 @@ static URLClassLoaderBuilder builder() return new URLClassLoaderBuilder(); } - URLClassLoaderBuilder setPaths( Collection paths ) + URLClassLoaderBuilder setLogger(Log logger ) + { + this.logger = logger; + return this; + } + + URLClassLoaderBuilder setExclusions(Collection exclusions ) + { + this.exclusions = exclusions; + return this; + } + + URLClassLoaderBuilder setPaths(Collection paths ) { this.paths = paths; return this; } - ClassLoader build() throws IOException + URLClassLoader build() throws IOException { List urls = new ArrayList<>( paths.size() ); for ( Path dependency : paths ) { + if ( exclusions != null && exclusions.contains( dependency.getFileName().toString() ) ) + { + if ( logger != null ) + { + logger.debug("Excluding as requested '" + dependency + "'" ); + } + continue; + } try { urls.add( dependency.toUri().toURL() ); @@ -68,7 +99,467 @@ ClassLoader build() throws IOException } } - return new URLClassLoader( urls.toArray( new URL[0] ) ); + return new ExecJavaClassLoader( urls.toArray( new URL[0] ) ); } + // child first strategy + private static class ExecJavaClassLoader extends URLClassLoader + { + static + { + try + { + registerAsParallelCapable(); + } + catch ( Exception e ) + { + // no-op, not that important + } + } + + private final String jre; + + public ExecJavaClassLoader(URL[] urls ) + { + super(urls); + jre = getJre(); + } + + @Override + public Class loadClass(final String name, final boolean resolve) throws ClassNotFoundException { + if (name == null) { + throw new ClassNotFoundException(); + } + + synchronized ( getClassLoadingLock( name ) ) + { + Class clazz; + + // if in the JVM, never override them + if ( isDirectJvmClass( name ) ) + { + try + { + clazz = getSystemClassLoader().loadClass( name ); + if ( postLoad( resolve, clazz ) ) + { + return clazz; + } + } + catch ( NoClassDefFoundError | ClassNotFoundException ignored ) + { + // no-op + } + } + + // already loaded? + clazz = findLoadedClass( name ); + if ( postLoad( resolve, clazz ) ) + { + return clazz; + } + + // look for it in this classloader + try + { + clazz = super.findClass( name ); + if (clazz != null) + { + if ( postLoad( resolve, clazz ) ) + { + return clazz; + } + return clazz; + } + } + catch ( ClassNotFoundException | NoClassDefFoundError ignored ) + { + // try next + } + + // load from parent - todo: exclude some classes? + ClassLoader parent = getParent(); + if (parent == null) { + parent = getSystemClassLoader(); + } + try + { + clazz = Class.forName( name, false, parent ); + if ( postLoad( resolve, clazz ) ) + { + return clazz; + } + } + catch ( ClassNotFoundException ignored ) + { + // no-op + } + + throw new ClassNotFoundException(name); + } + } + + @Override + public Enumeration getResources( String name ) throws IOException + { + final Enumeration selfResources = findResources(name); + final Enumeration parentResources = getParent().getResources(name); + if (!parentResources.hasMoreElements()) + { + return selfResources; + } + if (!selfResources.hasMoreElements()) + { + return new FilteringUrlEnum(parentResources); + } + return new ChainedEnumerations( + asList( selfResources, new FilteringUrlEnum(parentResources) ).iterator()); + } + + private boolean isInJvm( URL resource ) + { + final Path path = toPath(resource); + if (path == null) + { + return false; + } + return path.normalize().toAbsolutePath().toString().startsWith(jre); + } + + private String getJre() + { + final Path home = new File( System.getProperty("java.home", "") ).toPath(); + if ( "jre".equals( home.getFileName().toString() ) && home.getParent() != null && + Files.exists(home.getParent().resolve( "lib/tools.jar" ) ) ) + { + return home.getParent().toAbsolutePath().toString(); + } + return home.toAbsolutePath().toString(); + } + + private Path toPath(final URL url) + { + if ( "jar".equals( url.getProtocol()) ) + { + try + { + String spec = url.getFile(); + int separator = spec.indexOf( '!' ); + if ( separator == -1 ) { + return null; + } + return toPath( new URL( spec.substring( 0, separator + 1 ) ) ) ; + } + catch ( MalformedURLException e ) + { + // no-op + } + } + else if ( "file".equals( url.getProtocol() ) ) + { + String path = decode( url.getFile() ); + if (path.endsWith( "!" )) + { + path = path.substring( 0, path.length() - 1 ); + } + return new File(path).getAbsoluteFile().toPath(); + } + return null; + } + + private String decode( String fileName ) + { + if ( fileName.indexOf( '%' ) == -1 ) + { + return fileName; + } + + final StringBuilder result = new StringBuilder( fileName.length() ); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + for ( int i = 0; i < fileName.length(); ) + { + final char c = fileName.charAt( i ); + + if ( c == '%' ) + { + out.reset(); + do + { + if ( i + 2 >= fileName.length() ) + { + throw new IllegalArgumentException( "Incomplete % sequence at: " + i ); + } + + final int d1 = Character.digit( fileName.charAt( i + 1 ), 16 ); + final int d2 = Character.digit (fileName.charAt( i + 2 ), 16 ); + + if ( d1 == -1 || d2 == -1 ) + { + throw new IllegalArgumentException( + "Invalid % sequence (" + fileName.substring(i, i + 3) + ") at: " + i ); + } + + out.write( (byte) ( ( d1 << 4 ) + d2 ) ); + + i += 3; + + } + while ( i < fileName.length() && fileName.charAt(i) == '%' ); + + result.append( out.toString() ); + + continue; + } + else + { + result.append( c ); + } + + i++; + } + return result.toString(); + } + + private boolean postLoad( boolean resolve, Class clazz ) + { + if ( clazz != null ) + { + if (resolve) + { + resolveClass( clazz ); + } + return true; + } + return false; + } + + // not all jvm classes, for ex "javax" can be overriden so don't list it here + private boolean isDirectJvmClass(final String name) { + if (name.startsWith( "java." ) ) + { + return true; + } + if ( name.startsWith( "sun." ) ) + { + return true; + } + if ( name.startsWith( "jdk." ) ) + { + return true; + } + if ( name.startsWith( "oracle." ) ) + { + return true; + } + if ( name.startsWith( "javafx." ) ) + { + return true; + } + if ( name.startsWith( "netscape." ) ) + { + return true; + } + if ( name.startsWith( "org." ) ) + { + final String sub = name.substring( "org.".length() ); + if ( sub.startsWith( "w3c.dom." ) ) + { + return true; + } + if ( sub.startsWith( "omg." ) ) + { + return true; + } + if ( sub.startsWith( "xml.sax." ) ) + { + return true; + } + if ( sub.startsWith( "ietf.jgss." ) ) + { + return true; + } + if ( sub.startsWith( "jcp.xml.dsig.internal." ) ) + { + return true; + } + } + if ( name.startsWith( "com." ) ) + { + final String sub = name.substring( "com.".length() ); + if ( sub.startsWith( "oracle." ) ) + { + return true; + } + if ( sub.startsWith( "sun." ) ) + { + final String subSun = sub.substring( "sun.".length() ); + if ( subSun.startsWith( "accessibility." ) ) + { + return true; + } + if ( subSun.startsWith( "activation." ) ) + { + return true; + } + if ( subSun.startsWith( "awt." ) ) + { + return true; + } + if ( subSun.startsWith( "beans." ) ) + { + return true; + } + if ( subSun.startsWith( "corba.se." ) ) + { + return true; + } + if ( subSun.startsWith( "demo.jvmti." ) ) + { + return true; + } + if ( subSun.startsWith( "image.codec.jpeg." ) ) + { + return true; + } + if ( subSun.startsWith( "imageio." ) ) + { + return true; + } + if ( subSun.startsWith( "istack.internal." ) ) + { + return true; + } + if ( subSun.startsWith( "java." ) ) + { + return true; + } + if ( subSun.startsWith( "java_cup." ) ) + { + return true; + } + if ( subSun.startsWith( "jmx." ) ) + { + return true; + } + if ( subSun.startsWith( "jndi." ) ) + { + return true; + } + if ( subSun.startsWith( "management." ) ) + { + return true; + } + if ( subSun.startsWith( "media.sound." ) ) + { + return true; + } + if ( subSun.startsWith( "naming.internal." ) ) + { + return true; + } + if ( subSun.startsWith( "net." ) ) + { + return true; + } + if ( subSun.startsWith( "nio." ) ) + { + return true; + } + if ( subSun.startsWith( "org." ) ) + { + return true; + } + if ( subSun.startsWith( "rmi.rmid." ) ) + { + return true; + } + if ( subSun.startsWith( "rowset." ) ) + { + return true; + } + if ( subSun.startsWith( "security." ) ) + { + return true; + } + if ( subSun.startsWith( "swing." ) ) + { + return true; + } + if ( subSun.startsWith( "tracing." ) ) + { + return true; + } + if ( subSun.startsWith( "xml.internal." ) ) + { + return true; + } + return false; + } + } + return false; + } + + private class FilteringUrlEnum implements Enumeration + { + private final Enumeration delegate; + private URL next; + + private FilteringUrlEnum( Enumeration delegate ) + { + this.delegate = delegate; + } + + @Override + public boolean hasMoreElements() + { + while (delegate.hasMoreElements()) + { + next = delegate.nextElement(); + if (isInJvm(next)) + { + return true; + } + } + return false; + } + + @Override + public URL nextElement() + { + return next; + } + } + + private static class ChainedEnumerations implements Enumeration + { + private final Iterator> enumerations; + private Enumeration current; + + private ChainedEnumerations( Iterator> enumerations ) + { + this.enumerations = enumerations; + } + + @Override + public boolean hasMoreElements() + { + if (current == null || !current.hasMoreElements()) + { + if (!enumerations.hasNext()) + { + return false; + } + current = enumerations.next(); + } + return current.hasMoreElements(); + } + + @Override + public URL nextElement() + { + return current.nextElement(); + } + } + } } diff --git a/src/test/java/org/codehaus/mojo/exec/ExecJavaMojoTest.java b/src/test/java/org/codehaus/mojo/exec/ExecJavaMojoTest.java index f8ed07b0..da0346d1 100644 --- a/src/test/java/org/codehaus/mojo/exec/ExecJavaMojoTest.java +++ b/src/test/java/org/codehaus/mojo/exec/ExecJavaMojoTest.java @@ -18,9 +18,12 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.OutputStream; import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import org.apache.maven.execution.MavenSession; import org.apache.maven.monitor.logging.DefaultLog; @@ -239,12 +242,56 @@ public void testRunWithArgs() assertEquals( expectedResult, resultString ); } + /** + * Ensures that classpath can be filtered (exclude from plugin deps or project deps) to resolve conflicts. + * @throws Exception if something unexpected occurs. + */ + public void testExcludedClasspathElement() throws Exception + { + String LS = System.getProperty( "line.separator" ); + + // slf4j-simple + { + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + execute( + new File( getBasedir(), "src/test/projects/project16/pom.xml" ), "java", + stdout, stderr); + assertEquals( "org.slf4j.impl.SimpleLogger", stdout.toString().trim() ); + assertEquals( + "[org.codehaus.mojo.exec.Slf4jMain.main()] INFO org.codehaus.mojo.exec.Slf4jMain - hello[]" + LS, + stderr.toString() ); + } + + // slf4j-jdk14 + { + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + execute( + new File( getBasedir(), "src/test/projects/project17/pom.xml" ), "java", + stdout, stderr); + assertEquals( "org.slf4j.impl.JDK14LoggerAdapter", stdout.toString().trim() ); + final String stderrString = stderr.toString(); // simpler check, just validate it is not simple output + assertTrue( stderrString.contains( " org.codehaus.mojo.exec.Slf4jMain main" ) ); + assertTrue( stderrString.contains( ": hello[]" ) ); + } + } + /** * @return output from System.out during mojo execution */ private String execute( File pom, String goal ) throws Exception { + return execute( pom, goal, new ByteArrayOutputStream(), new ByteArrayOutputStream() ); + } + + /** + * @return output from System.out during mojo execution + */ + private String execute( File pom, String goal, ByteArrayOutputStream stringOutputStream, OutputStream stderr ) + throws Exception + { ExecJavaMojo mojo; mojo = (ExecJavaMojo) lookupMojo( goal, pom ); @@ -264,8 +311,9 @@ private String execute( File pom, String goal ) // trap System.out PrintStream out = System.out; - StringOutputStream stringOutputStream = new StringOutputStream(); + PrintStream err = System.err; System.setOut( new PrintStream( stringOutputStream ) ); + System.setErr( new PrintStream( stderr ) ); // ensure we don't log unnecessary stuff which would interfere with assessing success of tests mojo.setLog( new DefaultLog( new ConsoleLogger( Logger.LEVEL_ERROR, "exec:java" ) ) ); @@ -278,6 +326,7 @@ private String execute( File pom, String goal ) // see testUncooperativeThread() for explaination Thread.sleep( 300 ); // time seems about right System.setOut( out ); + System.setErr( err ); } return stringOutputStream.toString(); diff --git a/src/test/java/org/codehaus/mojo/exec/Slf4jMain.java b/src/test/java/org/codehaus/mojo/exec/Slf4jMain.java new file mode 100644 index 00000000..142580b8 --- /dev/null +++ b/src/test/java/org/codehaus/mojo/exec/Slf4jMain.java @@ -0,0 +1,25 @@ +package org.codehaus.mojo.exec; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.util.Arrays.asList; + +/** + * Simple class with a main method to call from unit tests + */ +public class Slf4jMain +{ + /** + * Print on stdout the logger class (slf4j binding) and then + * logs at info level args with Hello as suffix. + * + * @param args the arguments + */ + public static void main( String... args ) + { + final Logger logger = LoggerFactory.getLogger( Slf4jMain.class ); + System.out.println( logger.getClass().getName() ); + logger.info( "hello" + asList( args ) ); + } +} diff --git a/src/test/java/org/codehaus/mojo/exec/URLClassLoaderBuilderTest.java b/src/test/java/org/codehaus/mojo/exec/URLClassLoaderBuilderTest.java new file mode 100644 index 00000000..ff0898b8 --- /dev/null +++ b/src/test/java/org/codehaus/mojo/exec/URLClassLoaderBuilderTest.java @@ -0,0 +1,67 @@ +package org.codehaus.mojo.exec; + +/* + * 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.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; + +/** + * Basic tests about the custom classloader we set to execute the project. + */ +public class URLClassLoaderBuilderTest +{ + @Test + public void childFirst() throws Exception + { + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + PrintStream originalStderr = System.err; + Thread thread = Thread.currentThread(); + ClassLoader testLoader = thread.getContextClassLoader(); + try ( URLClassLoader loader = URLClassLoaderBuilder.builder() + .setPaths( asList( + Paths.get( "target/test-dependencies/slf4j-api.jar" ), + Paths.get( "target/test-dependencies/slf4j-jdk14.jar" ) + ) ) + .build(); + PrintStream tmpStderr = new PrintStream( stderr ) ) + { + System.setErr( tmpStderr ); + assertEquals( tmpStderr, System.err ); + thread.setContextClassLoader( loader ); + Class lf = loader.loadClass( "org.slf4j.LoggerFactory" ); + Object logger = lf.getMethod( "getLogger", Class.class ).invoke( null, String.class ); + assertEquals( "org.slf4j.impl.JDK14LoggerAdapter", logger.getClass().getName() ); + } + finally + { + thread.setContextClassLoader( testLoader ); + } + assertEquals("", new String( stderr.toByteArray(), StandardCharsets.UTF_8 ) ); + System.setErr( originalStderr ); + } +} diff --git a/src/test/projects/project16/pom.xml b/src/test/projects/project16/pom.xml new file mode 100644 index 00000000..fe5780ca --- /dev/null +++ b/src/test/projects/project16/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + org.cb.maven.plugins.exec + project16 + 0.1 + jar + Maven Exec Plugin + 2005 + + + + Apache License 2 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + test + + java + + + + + org.codehaus.mojo.exec.Slf4jMain + + + ../../../../target/test-classes + ../../../../target/test-dependencies/slf4j-api.jar + ../../../../target/test-dependencies/slf4j-jdk14.jar + ../../../../target/test-dependencies/slf4j-simple.jar + + + slf4j-jdk14.jar + + + + + + + diff --git a/src/test/projects/project17/pom.xml b/src/test/projects/project17/pom.xml new file mode 100644 index 00000000..0aa13adc --- /dev/null +++ b/src/test/projects/project17/pom.xml @@ -0,0 +1,43 @@ + + 4.0.0 + org.cb.maven.plugins.exec + project16 + 0.1 + jar + Maven Exec Plugin + 2005 + + + + Apache License 2 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + test + + java + + + + + org.codehaus.mojo.exec.Slf4jMain + + ../../../../target/test-classes + ../../../../target/test-dependencies/slf4j-api.jar + ../../../../target/test-dependencies/slf4j-jdk14.jar + + + + + + +