Skip to content

Commit 41bd72f

Browse files
gnodetJanMosigItemiskriegaex
authored
[MSHADE-366] "Access denied" during 'minimizeJar' (#161)
* [MSHADE-366] - "Access denied" during 'minimizeJar' Now ignoring directories when scanning the classpath for services. * [MSHADE-366] Refactor fix by @JanMosigItemis from #83 - Simplify Jan's solution from #83 in order to use 'continue' instead of nested 'if-else'. - Factor out two helper methods from 'removeServices', because that method was way too big to still be readable. - DRY-refactor Jan's new test cases into one checking two conditions. * Another attempt to clarify the problem - do not ignore directories, print a warning as before - ignore the project's build output directory which is always returned by getRuntimeClassPathElements() * Fix the test to work on all platforms, irrespective of the actual exception sent by the JDK Co-authored-by: Jan Mosig <jan.mosig@itemis.de> Co-authored-by: Alexander Kriegisch <Alexander@Kriegisch.name>
1 parent e342059 commit 41bd72f

File tree

2 files changed

+137
-70
lines changed

2 files changed

+137
-70
lines changed

src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java

Lines changed: 74 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -129,58 +129,22 @@ private void removeServices( final MavenProject project, final Clazzpath cp )
129129
neededClasses.removeAll( removable );
130130
try
131131
{
132+
// getRuntimeClasspathElements returns a list of
133+
// - the build output directory
134+
// - all the paths to the dependencies' jars
135+
// We thereby need to ignore the build directory because we don't want
136+
// to remove anything from it, as it's the starting point of the
137+
// minification process.
132138
for ( final String fileName : project.getRuntimeClasspathElements() )
133139
{
134-
try ( final JarFile jar = new JarFile( fileName ) )
140+
// Ignore the build directory from this project
141+
if ( fileName.equals( project.getBuild().getOutputDirectory() ) )
135142
{
136-
for ( final Enumeration<JarEntry> entries = jar.entries(); entries.hasMoreElements(); )
137-
{
138-
final JarEntry jarEntry = entries.nextElement();
139-
if ( jarEntry.isDirectory() || !jarEntry.getName().startsWith( "META-INF/services/" ) )
140-
{
141-
continue;
142-
}
143-
144-
final String serviceClassName =
145-
jarEntry.getName().substring( "META-INF/services/".length() );
146-
final boolean isNeededClass = neededClasses.contains( cp.getClazz( serviceClassName ) );
147-
if ( !isNeededClass )
148-
{
149-
continue;
150-
}
151-
152-
try ( final BufferedReader bufferedReader =
153-
new BufferedReader( new InputStreamReader( jar.getInputStream( jarEntry ), UTF_8 ) ) )
154-
{
155-
for ( String line = bufferedReader.readLine(); line != null;
156-
line = bufferedReader.readLine() )
157-
{
158-
final String className = line.split( "#", 2 )[0].trim();
159-
if ( className.isEmpty() )
160-
{
161-
continue;
162-
}
163-
164-
final Clazz clazz = cp.getClazz( className );
165-
if ( clazz == null || !removable.contains( clazz ) )
166-
{
167-
continue;
168-
}
169-
170-
log.debug( className + " was not removed because it is a service" );
171-
removeClass( clazz );
172-
repeatScan = true; // check whether the found classes use services in turn
173-
}
174-
}
175-
catch ( final IOException e )
176-
{
177-
log.warn( e.getMessage() );
178-
}
179-
}
143+
continue;
180144
}
181-
catch ( final IOException e )
145+
if ( removeServicesFromJar( cp, neededClasses, fileName ) )
182146
{
183-
log.warn( e.getMessage() );
147+
repeatScan = true;
184148
}
185149
}
186150
}
@@ -192,6 +156,69 @@ private void removeServices( final MavenProject project, final Clazzpath cp )
192156
while ( repeatScan );
193157
}
194158

159+
private boolean removeServicesFromJar( Clazzpath cp, Set<Clazz> neededClasses, String fileName )
160+
{
161+
boolean repeatScan = false;
162+
try ( final JarFile jar = new JarFile( fileName ) )
163+
{
164+
for ( final Enumeration<JarEntry> entries = jar.entries(); entries.hasMoreElements(); )
165+
{
166+
final JarEntry jarEntry = entries.nextElement();
167+
if ( jarEntry.isDirectory() || !jarEntry.getName().startsWith( "META-INF/services/" ) )
168+
{
169+
continue;
170+
}
171+
172+
final String serviceClassName = jarEntry.getName().substring( "META-INF/services/".length() );
173+
final boolean isNeededClass = neededClasses.contains( cp.getClazz( serviceClassName ) );
174+
if ( !isNeededClass )
175+
{
176+
continue;
177+
}
178+
179+
try ( final BufferedReader configFileReader = new BufferedReader(
180+
new InputStreamReader( jar.getInputStream( jarEntry ), UTF_8 ) ) )
181+
{
182+
// check whether the found classes use services in turn
183+
repeatScan = scanServiceProviderConfigFile( cp, configFileReader );
184+
}
185+
catch ( final IOException e )
186+
{
187+
log.warn( e.getMessage() );
188+
}
189+
}
190+
}
191+
catch ( final IOException e )
192+
{
193+
log.warn( "Not a JAR file candidate. Ignoring classpath element '" + fileName + "' (" + e + ")." );
194+
}
195+
return repeatScan;
196+
}
197+
198+
private boolean scanServiceProviderConfigFile( Clazzpath cp, BufferedReader configFileReader ) throws IOException
199+
{
200+
boolean serviceClassFound = false;
201+
for ( String line = configFileReader.readLine(); line != null; line = configFileReader.readLine() )
202+
{
203+
final String className = line.split( "#", 2 )[0].trim();
204+
if ( className.isEmpty() )
205+
{
206+
continue;
207+
}
208+
209+
final Clazz clazz = cp.getClazz( className );
210+
if ( clazz == null || !removable.contains( clazz ) )
211+
{
212+
continue;
213+
}
214+
215+
log.debug( className + " was not removed because it is a service" );
216+
removeClass( clazz );
217+
serviceClassFound = true;
218+
}
219+
return serviceClassFound;
220+
}
221+
195222
private void removeClass( final Clazz clazz )
196223
{
197224
removable.remove( clazz );

src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,40 +19,60 @@
1919
* under the License.
2020
*/
2121

22+
import static org.hamcrest.MatcherAssert.assertThat;
23+
import static org.hamcrest.Matchers.startsWith;
2224
import static org.junit.Assert.assertEquals;
25+
import static org.junit.Assert.fail;
2326
import static org.junit.Assume.assumeFalse;
2427
import static org.mockito.Mockito.mock;
2528
import static org.mockito.Mockito.times;
2629
import static org.mockito.Mockito.verify;
2730
import static org.mockito.Mockito.when;
2831

2932
import java.io.File;
33+
import java.io.FileOutputStream;
3034
import java.io.IOException;
35+
import java.util.ArrayList;
36+
import java.util.Arrays;
37+
import java.util.List;
3138
import java.util.Set;
3239
import java.util.TreeSet;
40+
import java.util.jar.JarOutputStream;
3341

3442
import org.apache.maven.artifact.Artifact;
3543
import org.apache.maven.artifact.DefaultArtifact;
44+
import org.apache.maven.artifact.DependencyResolutionRequiredException;
45+
import org.apache.maven.model.Build;
3646
import org.apache.maven.plugin.logging.Log;
3747
import org.apache.maven.project.MavenProject;
3848
import org.junit.Before;
49+
import org.junit.Rule;
3950
import org.junit.Test;
4051
import org.junit.rules.TemporaryFolder;
4152
import org.mockito.ArgumentCaptor;
4253

4354
public class MinijarFilterTest
4455
{
4556

57+
@Rule
58+
public TemporaryFolder tempFolder = TemporaryFolder.builder().assureDeletion().build();
59+
60+
private File outputDirectory;
4661
private File emptyFile;
62+
private File jarFile;
63+
private Log log;
64+
private ArgumentCaptor<CharSequence> logCaptor;
4765

4866
@Before
4967
public void init()
5068
throws IOException
5169
{
52-
TemporaryFolder tempFolder = new TemporaryFolder();
53-
tempFolder.create();
70+
this.outputDirectory = tempFolder.newFolder();
5471
this.emptyFile = tempFolder.newFile();
55-
72+
this.jarFile = tempFolder.newFile();
73+
new JarOutputStream( new FileOutputStream( this.jarFile ) ).close();
74+
this.log = mock(Log.class);
75+
logCaptor = ArgumentCaptor.forClass(CharSequence.class);
5676
}
5777

5878
/**
@@ -64,11 +84,7 @@ public void testWithMockProject()
6484
{
6585
assumeFalse( "Expected to run under JDK8+", System.getProperty("java.version").startsWith("1.7") );
6686

67-
ArgumentCaptor<CharSequence> logCaptor = ArgumentCaptor.forClass( CharSequence.class );
68-
69-
MavenProject mavenProject = mockProject( emptyFile );
70-
71-
Log log = mock( Log.class );
87+
MavenProject mavenProject = mockProject( outputDirectory, emptyFile );
7288

7389
MinijarFilter mf = new MinijarFilter( mavenProject, log );
7490

@@ -84,14 +100,10 @@ public void testWithMockProject()
84100
public void testWithPomProject()
85101
throws IOException
86102
{
87-
ArgumentCaptor<CharSequence> logCaptor = ArgumentCaptor.forClass( CharSequence.class );
88-
89103
// project with pom packaging and no artifact.
90-
MavenProject mavenProject = mockProject( null );
104+
MavenProject mavenProject = mockProject( outputDirectory, null );
91105
mavenProject.setPackaging( "pom" );
92106

93-
Log log = mock( Log.class );
94-
95107
MinijarFilter mf = new MinijarFilter( mavenProject, log );
96108

97109
mf.finished();
@@ -105,7 +117,7 @@ public void testWithPomProject()
105117

106118
}
107119

108-
private MavenProject mockProject( File file )
120+
private MavenProject mockProject( File outputDirectory, File file, String... classPathElements )
109121
{
110122
MavenProject mavenProject = mock( MavenProject.class );
111123

@@ -129,17 +141,29 @@ private MavenProject mockProject( File file )
129141

130142
when( mavenProject.getArtifact().getFile() ).thenReturn( file );
131143

132-
return mavenProject;
144+
Build build = new Build();
145+
build.setOutputDirectory( outputDirectory.toString() );
146+
147+
List<String> classpath = new ArrayList<>();
148+
classpath.add( outputDirectory.toString() );
149+
if ( file != null )
150+
{
151+
classpath.add(file.toString());
152+
}
153+
classpath.addAll( Arrays.asList( classPathElements ) );
154+
when( mavenProject.getBuild() ).thenReturn( build );
155+
try {
156+
when(mavenProject.getRuntimeClasspathElements()).thenReturn(classpath);
157+
} catch (DependencyResolutionRequiredException e) {
158+
fail("Encountered unexpected exception: " + e.getClass().getSimpleName() + ": " + e.getMessage());
159+
}
133160

161+
return mavenProject;
134162
}
135163

136164
@Test
137165
public void finsishedShouldProduceMessageForClassesTotalNonZero()
138166
{
139-
ArgumentCaptor<CharSequence> logCaptor = ArgumentCaptor.forClass( CharSequence.class );
140-
141-
Log log = mock( Log.class );
142-
143167
MinijarFilter m = new MinijarFilter( 1, 50, log );
144168

145169
m.finished();
@@ -153,10 +177,6 @@ public void finsishedShouldProduceMessageForClassesTotalNonZero()
153177
@Test
154178
public void finishedShouldProduceMessageForClassesTotalZero()
155179
{
156-
ArgumentCaptor<CharSequence> logCaptor = ArgumentCaptor.forClass( CharSequence.class );
157-
158-
Log log = mock( Log.class );
159-
160180
MinijarFilter m = new MinijarFilter( 0, 0, log );
161181

162182
m.finished();
@@ -166,4 +186,24 @@ public void finishedShouldProduceMessageForClassesTotalZero()
166186
assertEquals( "Minimized 0 -> 0", logCaptor.getValue() );
167187

168188
}
189+
190+
/**
191+
* Verify that directories are ignored when scanning the classpath for JARs containing services,
192+
* but warnings are logged instead
193+
*
194+
* @see <a href="https://issues.apache.org/jira/browse/MSHADE-366">MSHADE-366</a>
195+
*/
196+
@Test
197+
public void removeServicesShouldIgnoreDirectories() throws Exception {
198+
String classPathElementToIgnore = tempFolder.newFolder().getAbsolutePath();
199+
MavenProject mockedProject = mockProject( outputDirectory, jarFile, classPathElementToIgnore );
200+
201+
new MinijarFilter(mockedProject, log);
202+
203+
verify( log, times( 1 ) ).warn( logCaptor.capture() );
204+
205+
assertThat( logCaptor.getValue().toString(), startsWith(
206+
"Not a JAR file candidate. Ignoring classpath element '" + classPathElementToIgnore + "' (" ) );
207+
}
208+
169209
}

0 commit comments

Comments
 (0)