diff --git a/src/it/projects/MSHADE-313_minimized-services/invoker.properties b/src/it/projects/MSHADE-313_minimized-services/invoker.properties index c8b7b3cd..31f873fc 100644 --- a/src/it/projects/MSHADE-313_minimized-services/invoker.properties +++ b/src/it/projects/MSHADE-313_minimized-services/invoker.properties @@ -15,4 +15,5 @@ # specific language governing permissions and limitations # under the License. +# jdependency-2.6.0 needs Java 8+ invoker.java.version = 1.8+ diff --git a/src/it/projects/MSHADE-313_minimized-services/verify.bsh b/src/it/projects/MSHADE-313_minimized-services/verify.bsh index 2a58a847..2529359c 100644 --- a/src/it/projects/MSHADE-313_minimized-services/verify.bsh +++ b/src/it/projects/MSHADE-313_minimized-services/verify.bsh @@ -34,6 +34,8 @@ String[] wanted = String[] unwanted = { + // Unused SPI config files are not removed + //"META-INF/services/UnusedServiceInterface", "UnusedServiceInterface.class", "UnusedServiceClass.class", "SomeUnreferencedClass.class", diff --git a/src/it/projects/MSHADE-316/invoker.properties b/src/it/projects/MSHADE-316/invoker.properties index f1e7ddb1..31f873fc 100644 --- a/src/it/projects/MSHADE-316/invoker.properties +++ b/src/it/projects/MSHADE-316/invoker.properties @@ -5,9 +5,9 @@ # 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 @@ -15,5 +15,5 @@ # specific language governing permissions and limitations # under the License. -#jdependency-2.1.1 is Java8 compatible +# jdependency-2.6.0 needs Java 8+ invoker.java.version = 1.8+ diff --git a/src/it/projects/MSHADE-400_self-minimized-services/invoker.properties b/src/it/projects/MSHADE-400_self-minimized-services/invoker.properties new file mode 100644 index 00000000..31f873fc --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/invoker.properties @@ -0,0 +1,19 @@ +# 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. + +# jdependency-2.6.0 needs Java 8+ +invoker.java.version = 1.8+ diff --git a/src/it/projects/MSHADE-400_self-minimized-services/pom.xml b/src/it/projects/MSHADE-400_self-minimized-services/pom.xml new file mode 100644 index 00000000..b48424d9 --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/pom.xml @@ -0,0 +1,60 @@ + + + + + + 4.0.0 + + org.acme + module-with-services + 1.0 + + + 8 + 8 + + + + + + org.apache.maven.plugins + maven-shade-plugin + @project.version@ + + + shade + package + + shade + + + true + + org.acme.Application + + + + + + + + + diff --git a/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/Application.java b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/Application.java new file mode 100644 index 00000000..1dfaeda9 --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/Application.java @@ -0,0 +1,31 @@ +/* + * 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.acme; + +import java.util.ServiceLoader; + +public class Application +{ + private UsedClass usedClass = new UsedClass(); + + public static void main( String[] args ) + { + ServiceLoader.load( UsedService.class ); + } +} diff --git a/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UnusedClass.java b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UnusedClass.java new file mode 100644 index 00000000..500782b9 --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UnusedClass.java @@ -0,0 +1,23 @@ +/* + * 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.acme; + +public class UnusedClass +{ +} diff --git a/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UnusedService.java b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UnusedService.java new file mode 100644 index 00000000..7f6410ac --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UnusedService.java @@ -0,0 +1,24 @@ +/* + * 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.acme; + +public interface UnusedService +{ + public void doSomething(); +} diff --git a/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UnusedServiceImplA.java b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UnusedServiceImplA.java new file mode 100644 index 00000000..0269a720 --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UnusedServiceImplA.java @@ -0,0 +1,27 @@ +/* + * 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.acme; + +public class UnusedServiceImplA implements UnusedService +{ + @Override + public void doSomething() + { + } +} diff --git a/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UnusedServiceImplB.java b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UnusedServiceImplB.java new file mode 100644 index 00000000..dc1d3d60 --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UnusedServiceImplB.java @@ -0,0 +1,27 @@ +/* + * 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.acme; + +public class UnusedServiceImplB implements UnusedService +{ + @Override + public void doSomething() + { + } +} diff --git a/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UsedClass.java b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UsedClass.java new file mode 100644 index 00000000..15315216 --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UsedClass.java @@ -0,0 +1,23 @@ +/* + * 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.acme; + +public class UsedClass +{ +} diff --git a/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UsedService.java b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UsedService.java new file mode 100644 index 00000000..23c28405 --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UsedService.java @@ -0,0 +1,24 @@ +/* + * 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.acme; + +public interface UsedService +{ + public void doSomething(); +} diff --git a/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UsedServiceUnusedImpl.java b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UsedServiceUnusedImpl.java new file mode 100644 index 00000000..800ddf07 --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UsedServiceUnusedImpl.java @@ -0,0 +1,27 @@ +/* + * 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.acme; + +public class UsedServiceUnusedImpl implements UsedService +{ + @Override + public void doSomething() + { + } +} diff --git a/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UsedServiceUsedImpl.java b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UsedServiceUsedImpl.java new file mode 100644 index 00000000..345235c3 --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/src/main/java/org/acme/UsedServiceUsedImpl.java @@ -0,0 +1,27 @@ +/* + * 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.acme; + +public class UsedServiceUsedImpl implements UsedService +{ + @Override + public void doSomething() + { + } +} diff --git a/src/it/projects/MSHADE-400_self-minimized-services/src/main/resources/META-INF/services/org.acme.UnusedService b/src/it/projects/MSHADE-400_self-minimized-services/src/main/resources/META-INF/services/org.acme.UnusedService new file mode 100644 index 00000000..c4f9684d --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/src/main/resources/META-INF/services/org.acme.UnusedService @@ -0,0 +1,22 @@ +# +# 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. +# + +# These services are defined, but not used in the entry point or any of its dependency classes +org.acme.UnusedServiceUsedImplA +org.acme.UnusedServiceUsedImplB diff --git a/src/it/projects/MSHADE-400_self-minimized-services/src/main/resources/META-INF/services/org.acme.UsedService b/src/it/projects/MSHADE-400_self-minimized-services/src/main/resources/META-INF/services/org.acme.UsedService new file mode 100644 index 00000000..0cf82b09 --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/src/main/resources/META-INF/services/org.acme.UsedService @@ -0,0 +1,23 @@ +# +# 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. +# + +org.acme.UsedServiceUsedImpl + +# This implementation is *not* used: +# org.acme.UsedServiceUnusedImpl diff --git a/src/it/projects/MSHADE-400_self-minimized-services/verify.bsh b/src/it/projects/MSHADE-400_self-minimized-services/verify.bsh new file mode 100644 index 00000000..2708621c --- /dev/null +++ b/src/it/projects/MSHADE-400_self-minimized-services/verify.bsh @@ -0,0 +1,60 @@ +/* + * 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.*; +import java.util.jar.*; + +String[] wanted = +{ + "META-INF/services/org.acme.UsedService", + "org/acme/Application.class", + "org/acme/UsedClass.class", + "org/acme/UsedService.class", + "org/acme/UsedServiceUsedImpl.class" +}; + +String[] unwanted = +{ + // Unused SPI config files are not removed + //"META-INF/services/org.acme.UnusedService", + "org/acme/UsedServiceUnusedImpl.class", + "org/acme/UnusedClass.class", + "org/acme/UnusedService.class", + "org/acme/UnusedServiceImplA.class", + "org/acme/UnusedServiceImplB.class" +}; + +JarFile jarFile = new JarFile( new File( basedir, "target/module-with-services-1.0.jar" ) ); + +for ( String path : wanted ) +{ + if ( jarFile.getEntry( path ) == null ) + { + throw new IllegalStateException( "wanted path is missing: " + path ); + } +} + +for ( String path : unwanted ) +{ + if ( jarFile.getEntry( path ) != null ) + { + throw new IllegalStateException( "unwanted path is present: " + path ); + } +} + +jarFile.close(); diff --git a/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java b/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java index b0be384b..16f20d9c 100644 --- a/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java +++ b/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java @@ -22,6 +22,7 @@ import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -40,6 +41,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.concurrent.Callable; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; @@ -236,60 +238,142 @@ private void shadeJars( ShadeRequest shadeRequest, Set resources, List jarFilters = getFilters( jar, shadeRequest.getFilters() ); - - try ( JarFile jarFile = newJarFile( jar ) ) + if ( jar.isDirectory() ) + { + shadeDir( shadeRequest, resources, transformers, packageMapper, jos, duplicates, + jar, jar, "", jarFilters ); + } + else { + shadeJar( shadeRequest, resources, transformers, packageMapper, jos, duplicates, + jar, jarFilters ); + } + } + } - for ( Enumeration j = jarFile.entries(); j.hasMoreElements(); ) + private void shadeDir( ShadeRequest shadeRequest, Set resources, + List transformers, DefaultPackageMapper packageMapper, + JarOutputStream jos, MultiValuedMap duplicates, + File jar, File current, String prefix, List jarFilters ) throws IOException + { + final File[] children = current.listFiles(); + if ( children == null ) + { + return; + } + for ( final File file : children ) + { + final String name = prefix + file.getName(); + if ( file.isDirectory() ) + { + try + { + shadeDir( + shadeRequest, resources, transformers, packageMapper, jos, + duplicates, jar, file, + prefix + file.getName() + '/', jarFilters ); + continue; + } + catch ( Exception e ) { - JarEntry entry = j.nextElement(); + throw new IOException( + String.format( "Problem shading JAR %s entry %s: %s", current, name, e ), e ); + } + } + if ( isFiltered( jarFilters, name ) || isExcludedEntry( name ) ) + { + continue; + } - String name = entry.getName(); - - if ( entry.isDirectory() || isFiltered( jarFilters, name ) ) - { - continue; - } + try + { + shadeJarEntry( + shadeRequest, resources, transformers, packageMapper, jos, duplicates, jar, + new Callable() + { + @Override + public InputStream call() throws Exception + { + return new FileInputStream( file ); + } + }, name, file.lastModified(), -1 /*ignore*/ ); + } + catch ( Exception e ) + { + throw new IOException( String.format( "Problem shading JAR %s entry %s: %s", current, name, e ), + e ); + } + } + } + private void shadeJar( ShadeRequest shadeRequest, Set resources, + List transformers, DefaultPackageMapper packageMapper, + JarOutputStream jos, MultiValuedMap duplicates, + File jar, List jarFilters ) throws IOException + { + try ( JarFile jarFile = newJarFile( jar ) ) + { - if ( "META-INF/INDEX.LIST".equals( name ) ) - { - // we cannot allow the jar indexes to be copied over or the - // jar is useless. Ideally, we could create a new one - // later - continue; - } + for ( Enumeration j = jarFile.entries(); j.hasMoreElements(); ) + { + final JarEntry entry = j.nextElement(); - if ( "module-info.class".equals( name ) ) - { - logger.warn( "Discovered module-info.class. " - + "Shading will break its strong encapsulation." ); - continue; - } + String name = entry.getName(); - try - { - shadeJarEntry( shadeRequest, resources, transformers, packageMapper, jos, duplicates, jar, - jarFile, entry, name ); - } - catch ( Exception e ) - { - throw new IOException( String.format( "Problem shading JAR %s entry %s: %s", jar, name, e ), - e ); - } + if ( entry.isDirectory() || isFiltered( jarFilters, name ) || isExcludedEntry( name ) ) + { + continue; } + try + { + shadeJarEntry( + shadeRequest, resources, transformers, packageMapper, jos, duplicates, jar, + new Callable() + { + @Override + public InputStream call() throws Exception + { + return jarFile.getInputStream( entry ); + } + }, name, entry.getTime(), entry.getMethod() ); + } + catch ( Exception e ) + { + throw new IOException( String.format( "Problem shading JAR %s entry %s: %s", jar, name, e ), + e ); + } } + } } + private boolean isExcludedEntry( final String name ) + { + if ( "META-INF/INDEX.LIST".equals( name ) ) + { + // we cannot allow the jar indexes to be copied over or the + // jar is useless. Ideally, we could create a new one + // later + return true; + } + + if ( "module-info.class".equals( name ) ) + { + logger.warn( "Discovered module-info.class. " + + "Shading will break its strong encapsulation." ); + return true; + } + return false; + } + private void shadeJarEntry( ShadeRequest shadeRequest, Set resources, List transformers, DefaultPackageMapper packageMapper, JarOutputStream jos, MultiValuedMap duplicates, File jar, - JarFile jarFile, JarEntry entry, String name ) - throws IOException, MojoExecutionException + Callable inputProvider, String name, long time, int method ) + throws Exception { - try ( InputStream in = jarFile.getInputStream( entry ) ) + try ( InputStream in = inputProvider.call() ) { String mappedName = packageMapper.map( name, true, false ); @@ -300,14 +384,14 @@ private void shadeJarEntry( ShadeRequest shadeRequest, Set resources, String dir = mappedName.substring( 0, idx ); if ( !resources.contains( dir ) ) { - addDirectory( resources, jos, dir, entry.getTime() ); + addDirectory( resources, jos, dir, time ); } } duplicates.put( name, jar ); if ( name.endsWith( ".class" ) ) { - addRemappedClass( jos, jar, name, entry.getTime(), in, packageMapper ); + addRemappedClass( jos, jar, name, time, in, packageMapper ); } else if ( shadeRequest.isShadeSourcesContent() && name.endsWith( ".java" ) ) { @@ -317,12 +401,12 @@ else if ( shadeRequest.isShadeSourcesContent() && name.endsWith( ".java" ) ) return; } - addJavaSource( resources, jos, mappedName, entry.getTime(), in, shadeRequest.getRelocators() ); + addJavaSource( resources, jos, mappedName, time, in, shadeRequest.getRelocators() ); } else { if ( !resourceTransformed( transformers, mappedName, in, shadeRequest.getRelocators(), - entry.getTime() ) ) + time ) ) { // Avoid duplicates that aren't accounted for by the resource transformers if ( resources.contains( mappedName ) ) @@ -331,7 +415,7 @@ else if ( shadeRequest.isShadeSourcesContent() && name.endsWith( ".java" ) ) return; } - addResource( resources, jos, mappedName, entry, jarFile ); + addResource( resources, jos, mappedName, inputProvider, time, method ); } else { @@ -537,12 +621,12 @@ private void addRemappedClass( JarOutputStream jos, File jar, String name, return; } - - // Keep the original class in, in case nothing was relocated by RelocatorRemapper. This avoids binary + + // Keep the original class, in case nothing was relocated by ShadeClassRemapper. This avoids binary // differences between classes, simply because they were rewritten and only details like constant pool or // stack map frames are slightly different. byte[] originalClass = IOUtil.toByteArray( is ); - + ClassReader cr = new ClassReader( new ByteArrayInputStream( originalClass ) ); // We don't pass the ClassReader here. This forces the ClassWriter to rebuild the constant pool. @@ -564,7 +648,7 @@ private void addRemappedClass( JarOutputStream jos, File jar, String name, throw new MojoExecutionException( "Error in ASM processing class " + name, ise ); } - // If nothing was relocated by RelocatorRemapper, write the original class, otherwise the transformed one + // If nothing was relocated by ShadeClassRemapper, write the original class, otherwise the transformed one final byte[] renamedClass; if ( cv.remapped ) { @@ -659,24 +743,24 @@ private void addJavaSource( Set resources, JarOutputStream jos, String n resources.add( name ); } - private void addResource( Set resources, JarOutputStream jos, String name, JarEntry originalEntry, - JarFile jarFile ) throws IOException + private void addResource( Set resources, JarOutputStream jos, String name, Callable input, + long time, int method ) throws Exception { - ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream( jarFile.getInputStream( originalEntry ) ); + ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream( input.call() ); try { final JarEntry entry = new JarEntry( name ); // We should not change compressed level of uncompressed entries, otherwise JVM can't load these nested jars - if ( inputStream.hasZipHeader() && originalEntry.getMethod() == ZipEntry.STORED ) + if ( inputStream.hasZipHeader() && method == ZipEntry.STORED ) { new CrcAndSize( inputStream ).setupStoredEntry( entry ); inputStream.close(); - inputStream = new ZipHeaderPeekInputStream( jarFile.getInputStream( originalEntry ) ); + inputStream = new ZipHeaderPeekInputStream( input.call() ); } - entry.setTime( originalEntry.getTime() ); + entry.setTime( time ); jos.putNextEntry( entry ); @@ -694,7 +778,7 @@ private interface PackageMapper { /** * Map an entity name according to the mapping rules known to this package mapper - * + * * @param entityName entity name to be mapped * @param mapPaths map "slashy" names like paths or internal Java class names, e.g. {@code com/acme/Foo}? * @param mapPackages map "dotty" names like qualified Java class or package names, e.g. {@code com.acme.Foo}? diff --git a/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java b/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java index a996bd46..0ce1c875 100644 --- a/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java +++ b/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java @@ -77,17 +77,30 @@ public class MinijarFilter public MinijarFilter( MavenProject project, Log log ) throws IOException { - this( project, log, Collections.emptyList() ); + this( project, log, Collections.emptyList(), Collections.emptySet() ); + } + + /** + * @param project {@link MavenProject} + * @param log {@link Log} + * @param entryPoints + * @throws IOException in case of error. + */ + public MinijarFilter( MavenProject project, Log log, Set entryPoints ) + throws IOException + { + this( project, log, Collections.emptyList(), entryPoints ); } /** * @param project {@link MavenProject} * @param log {@link Log} * @param simpleFilters {@link SimpleFilter} + * @param entryPoints * @throws IOException in case of errors. * @since 1.6 */ - public MinijarFilter( MavenProject project, Log log, List simpleFilters ) + public MinijarFilter( MavenProject project, Log log, List simpleFilters, Set entryPoints ) throws IOException { this.log = log; @@ -111,8 +124,44 @@ public MinijarFilter( MavenProject project, Log log, List simpleFi log.warn( "Removing module-info from " + artifactFile.getName() ); } removePackages( artifactUnit ); - removable.removeAll( artifactUnit.getClazzes() ); - removable.removeAll( artifactUnit.getTransitiveDependencies() ); + if ( entryPoints.isEmpty() ) + { + removable.removeAll( artifactUnit.getClazzes() ); + removable.removeAll( artifactUnit.getTransitiveDependencies() ); + } + else + { + Set artifactUnitClazzes = artifactUnit.getClazzes(); + Set entryPointsToKeep = new HashSet<>(); + for ( String entryPoint : entryPoints ) + { + Clazz entryPointFound = null; + for ( Clazz clazz : artifactUnitClazzes ) + { + if ( clazz.getName().equals( entryPoint ) ) + { + entryPointFound = clazz; + break; + } + } + if ( entryPointFound != null ) + { + entryPointsToKeep.add( entryPointFound ); + } + } + removable.removeAll( entryPointsToKeep ); + if ( entryPointsToKeep.isEmpty() ) + { + removable.removeAll( artifactUnit.getTransitiveDependencies() ); + } + else + { + for ( Clazz entryPoint : entryPointsToKeep ) + { + removable.removeAll( entryPoint.getTransitiveDependencies() ); + } + } + } removeSpecificallyIncludedClasses( project, simpleFilters == null ? Collections.emptyList() : simpleFilters ); removeServices( project, cp ); @@ -137,14 +186,13 @@ private void removeServices( final MavenProject project, final Clazzpath cp ) // minification process. for ( final String fileName : project.getRuntimeClasspathElements() ) { - // Ignore the build directory from this project - if ( fileName.equals( project.getBuild().getOutputDirectory() ) ) + if ( new File( fileName ).isDirectory() ) { - continue; + repeatScan |= removeServicesFromDir( cp, neededClasses, fileName ); } - if ( removeServicesFromJar( cp, neededClasses, fileName ) ) + else { - repeatScan = true; + repeatScan |= removeServicesFromJar( cp, neededClasses, fileName ); } } } @@ -156,6 +204,43 @@ private void removeServices( final MavenProject project, final Clazzpath cp ) while ( repeatScan ); } + private boolean removeServicesFromDir( Clazzpath cp, Set neededClasses, String fileName ) + { + final File servicesDir = new File( fileName, "META-INF/services/" ); + if ( !servicesDir.isDirectory() ) + { + return false; + } + final File[] serviceProviderConfigFiles = servicesDir.listFiles(); + if ( serviceProviderConfigFiles == null || serviceProviderConfigFiles.length == 0 ) + { + return false; + } + + boolean repeatScan = false; + for ( File serviceProviderConfigFile : serviceProviderConfigFiles ) + { + final String serviceClassName = serviceProviderConfigFile.getName(); + final boolean isNeededClass = neededClasses.contains( cp.getClazz( serviceClassName ) ); + if ( !isNeededClass ) + { + continue; + } + + try ( final BufferedReader configFileReader = new BufferedReader( + new InputStreamReader( new FileInputStream( serviceProviderConfigFile ), UTF_8 ) ) ) + { + // check whether the found classes use services in turn + repeatScan |= scanServiceProviderConfigFile( cp, configFileReader ); + } + catch ( final IOException e ) + { + log.warn( e.getMessage() ); + } + } + return repeatScan; + } + private boolean removeServicesFromJar( Clazzpath cp, Set neededClasses, String fileName ) { boolean repeatScan = false; diff --git a/src/main/java/org/apache/maven/plugins/shade/mojo/ShadeMojo.java b/src/main/java/org/apache/maven/plugins/shade/mojo/ShadeMojo.java index 40f712c7..c83d27cb 100644 --- a/src/main/java/org/apache/maven/plugins/shade/mojo/ShadeMojo.java +++ b/src/main/java/org/apache/maven/plugins/shade/mojo/ShadeMojo.java @@ -111,7 +111,7 @@ public class ShadeMojo * syntax groupId is equivalent to groupId:*:*:*, groupId:artifactId is * equivalent to groupId:artifactId:*:* and groupId:artifactId:classifier is equivalent to * groupId:artifactId:*:classifier. For example: - * + * *
      * <artifactSet>
      *   <includes>
@@ -128,7 +128,7 @@ public class ShadeMojo
 
     /**
      * Packages to be relocated. For example:
-     * 
+     *
      * 
      * <relocations>
      *   <relocation>
@@ -143,7 +143,7 @@ public class ShadeMojo
      *   </relocation>
      * </relocations>
      * 
- * + * * Note: Support for includes exists only since version 1.4. */ @SuppressWarnings( "MismatchedReadAndWriteOfArray" ) @@ -164,7 +164,7 @@ public class ShadeMojo * to use an include to collect a set of files from the archive then use excludes to further reduce the set. By * default, all files are included and no files are excluded. If multiple filters apply to an artifact, the * intersection of the matched files will be included in the final JAR. For example: - * + * *
      * <filters>
      *   <filter>
@@ -310,13 +310,41 @@ public class ShadeMojo
 
     /**
      * When true, dependencies will be stripped down on the class level to only the transitive hull required for the
-     * artifact. Note: Usage of this feature requires Java 1.5 or higher.
+     * artifact. See also {@link #entryPoints}, if you wish to further optimize JAR minimization.
+     * 

+ * Note: This feature requires Java 1.8 or higher due to its use of + * jdependency. Its accuracy therefore also depends on + * jdependency's limitations. * * @since 1.4 */ @Parameter private boolean minimizeJar; + /** + * Use this option in order to fine-tune {@link #minimizeJar}: By default, all of the target module's classes are + * kept and used as entry points for JAR minimization. By explicitly limiting the set of entry points, you can + * further minimize the set of classes kept in the shaded JAR. This affects both classes in the module itself and + * dependency classes. If {@link #minimizeJar} is inactive, this option has no effect either. + *

+ * Note: This feature requires Java 1.8 or higher due to its use of + * jdependency. Its accuracy therefore also depends on + * jdependency's limitations. + *

+ * Configuration example: + *

{@code
+     * true
+     * 
+     *   org.acme.Application
+     *   org.acme.OtherEntryPoint
+     * 
+     * }
+ * + * @since 3.5.0 + */ + @Parameter + private Set entryPoints; + /** * The path to the output file for the shaded artifact. When this parameter is set, the created archive will neither * replace the project's main artifact nor will it be attached. Hence, this parameter causes the parameters @@ -551,7 +579,7 @@ public void execute() replaceFile( finalFile, testSourcesJar ); testSourcesJar = finalFile; } - + renamed = true; } @@ -964,11 +992,16 @@ private List getFilters() if ( minimizeJar ) { - getLog().info( "Minimizing jar " + project.getArtifact() ); + if ( entryPoints == null ) + { + entryPoints = new HashSet<>(); + } + getLog().info( "Minimizing jar " + project.getArtifact() + + ( entryPoints.isEmpty() ? "" : " with entry points" ) ); try { - filters.add( new MinijarFilter( project, getLog(), simpleFilters ) ); + filters.add( new MinijarFilter( project, getLog(), simpleFilters, entryPoints ) ); } catch ( IOException e ) { @@ -1153,7 +1186,7 @@ private void rewriteDependencyReducedPomIfWeHaveReduction( List depe } File f = dependencyReducedPomLocation; - // MSHADE-225 + // MSHADE-225 // Works for now, maybe there's a better algorithm where no for-loop is required if ( loopCounter == 0 ) { diff --git a/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java b/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java index 14786ecc..960cf8d1 100644 --- a/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java +++ b/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java @@ -25,6 +25,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.InputStreamReader; import java.lang.reflect.Field; import java.net.URL; @@ -36,11 +37,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Enumeration; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import java.util.stream.Collectors; import java.util.zip.CRC32; @@ -57,6 +60,7 @@ import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.Os; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.mockito.ArgumentCaptor; @@ -65,6 +69,9 @@ import org.objectweb.asm.Opcodes; import org.slf4j.Logger; +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static org.codehaus.plexus.util.FileUtils.forceMkdir; import static java.util.Objects.requireNonNull; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.hasItem; @@ -87,6 +94,9 @@ public class DefaultShaderTest private static final String[] EXCLUDES = new String[] { "org/codehaus/plexus/util/xml/Xpp3Dom", "org/codehaus/plexus/util/xml/pull.*" }; + @ClassRule + public static final TemporaryFolder tmp = new TemporaryFolder(); + private final String NEWLINE = "\n"; @Test @@ -119,7 +129,7 @@ public void testNoopWhenNotRelocated() throws IOException, MojoExecutionExceptio // Before MSHADE-391, the processed files were written to the uber JAR, which did no harm, but made it // difficult to find out by simple file comparison, if a file was actually relocated or not. Now, Shade // makes sure to always write the original file if the class neither was relocated itself nor references - // other, relocated classes. So we are checking for regressions here. + // other, relocated classes. So we are checking for regressions here. assertTrue( areEqual( originalJar, shadedJar, "org/codehaus/plexus/util/Expand.class" ) ); @@ -289,6 +299,60 @@ public void testShaderWithoutExcludesShouldRemoveReferencesOfOriginalPattern() new String[] {} ); } + @Test + public void testHandleDirectory() + throws Exception + { + final File dir = tmp.getRoot(); + // explode src/test/jars/test-artifact-1.0-SNAPSHOT.jar in this temp dir + try ( final JarInputStream in = new JarInputStream( + new FileInputStream( "src/test/jars/test-artifact-1.0-SNAPSHOT.jar" ) ) ) + { + JarEntry nextJarEntry; + while ( (nextJarEntry = in.getNextJarEntry()) != null ) + { + if ( nextJarEntry.isDirectory() ) + { + continue; + } + final File out = new File( dir, nextJarEntry.getName() ); + forceMkdir( out.getParentFile() ); + try ( final OutputStream outputStream = new FileOutputStream( out ) ) + { + IOUtil.copy( in, outputStream, (int) Math.max( nextJarEntry.getSize(), 512 ) ); + } + } + } + + // do shade + final File shade = new File( "target/testHandleDirectory.jar" ); + shaderWithPattern( "org/shaded/plexus/util", shade, new String[0], singleton( dir ) ); + + // ensure directory was shaded properly + try ( final JarFile jar = new JarFile( shade ) ) + { + final List entries = new ArrayList<>(); + final Enumeration jarEntryEnumeration = jar.entries(); + while ( jarEntryEnumeration.hasMoreElements() ) + { + final JarEntry jarEntry = jarEntryEnumeration.nextElement(); + if ( jarEntry.isDirectory() ) + { + continue; + } + entries.add( jarEntry.getName() ); + } + Collections.sort( entries ); + assertEquals( + asList( + "META-INF/maven/org.apache.maven.plugins.shade/test-artifact/pom.properties", + "META-INF/maven/org.apache.maven.plugins.shade/test-artifact/pom.xml", + "org/apache/maven/plugins/shade/Lib.class" + ), + entries ); + } + } + @Test public void testShaderWithRelocatedClassname() throws Exception @@ -531,16 +595,18 @@ private void writeEntryWithoutCompression( String entryName, byte[] entryBytes, jos.closeEntry(); } - private void shaderWithPattern( String shadedPattern, File jar, String[] excludes ) - throws Exception + private void shaderWithPattern( String shadedPattern, File jar, String[] excludes ) throws Exception { - DefaultShader s = newShader(); - Set set = new LinkedHashSet<>(); - set.add( new File( "src/test/jars/test-project-1.0-SNAPSHOT.jar" ) ); - set.add( new File( "src/test/jars/plexus-utils-1.4.1.jar" ) ); + shaderWithPattern( shadedPattern, jar, excludes, set ); + } + + private void shaderWithPattern( String shadedPattern, File jar, String[] excludes, Set set ) + throws Exception + { + DefaultShader s = newShader(); List relocators = new ArrayList<>(); diff --git a/src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java b/src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java index 25be4d8a..9f13a12d 100644 --- a/src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java +++ b/src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java @@ -32,6 +32,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -70,7 +71,7 @@ public void init() this.outputDirectory = tempFolder.newFolder(); this.emptyFile = tempFolder.newFile(); this.jarFile = tempFolder.newFile(); - new JarOutputStream( new FileOutputStream( this.jarFile ) ).close(); + new JarOutputStream(Files.newOutputStream(this.jarFile.toPath())).close(); this.log = mock(Log.class); logCaptor = ArgumentCaptor.forClass(CharSequence.class); } @@ -187,23 +188,4 @@ public void finishedShouldProduceMessageForClassesTotalZero() } - /** - * Verify that directories are ignored when scanning the classpath for JARs containing services, - * but warnings are logged instead - * - * @see MSHADE-366 - */ - @Test - public void removeServicesShouldIgnoreDirectories() throws Exception { - String classPathElementToIgnore = tempFolder.newFolder().getAbsolutePath(); - MavenProject mockedProject = mockProject( outputDirectory, jarFile, classPathElementToIgnore ); - - new MinijarFilter(mockedProject, log); - - verify( log, times( 1 ) ).warn( logCaptor.capture() ); - - assertThat( logCaptor.getValue().toString(), startsWith( - "Not a JAR file candidate. Ignoring classpath element '" + classPathElementToIgnore + "' (" ) ); - } - }