Skip to content

Commit 8c76381

Browse files
committed
PathMatchingResourcePatternResolver supports "classpath*" searches in jar file roots as well
Issue: SPR-12095
1 parent 0c32d66 commit 8c76381

File tree

3 files changed

+103
-18
lines changed

3 files changed

+103
-18
lines changed

spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
import java.lang.reflect.InvocationHandler;
2222
import java.lang.reflect.Method;
2323
import java.net.JarURLConnection;
24+
import java.net.MalformedURLException;
2425
import java.net.URISyntaxException;
2526
import java.net.URL;
27+
import java.net.URLClassLoader;
2628
import java.net.URLConnection;
2729
import java.util.Collections;
2830
import java.util.Enumeration;
@@ -310,12 +312,17 @@ protected Resource[] findAllClassPathResources(String location) throws IOExcepti
310312
URL url = resourceUrls.nextElement();
311313
result.add(convertClassLoaderURL(url));
312314
}
315+
if ("".equals(path)) {
316+
// The above result is likely to be incomplete, i.e. only containing file system references.
317+
// We need to have pointers to each of the jar files on the classpath as well...
318+
addAllClassLoaderJarRoots(cl, result);
319+
}
313320
return result.toArray(new Resource[result.size()]);
314321
}
315322

316323
/**
317-
* Convert the given URL as returned from the ClassLoader into a Resource object.
318-
* <p>The default implementation simply creates a UrlResource instance.
324+
* Convert the given URL as returned from the ClassLoader into a {@link Resource}.
325+
* <p>The default implementation simply creates a {@link UrlResource} instance.
319326
* @param url a URL as returned from the ClassLoader
320327
* @return the corresponding Resource object
321328
* @see java.lang.ClassLoader#getResources
@@ -325,6 +332,53 @@ protected Resource convertClassLoaderURL(URL url) {
325332
return new UrlResource(url);
326333
}
327334

335+
/**
336+
* Search all {@link URLClassLoader} URLs for jar file references and add them to the
337+
* given set of resources in the form of pointers to the root of the jar file content.
338+
* @param classLoader the ClassLoader to search (including its ancestors)
339+
* @param result the set of resources to add jar roots to
340+
*/
341+
private void addAllClassLoaderJarRoots(ClassLoader classLoader, Set<Resource> result) {
342+
if (classLoader instanceof URLClassLoader) {
343+
try {
344+
for (URL url : ((URLClassLoader) classLoader).getURLs()) {
345+
if (ResourceUtils.isJarFileURL(url)) {
346+
try {
347+
UrlResource jarResource = new UrlResource(
348+
ResourceUtils.JAR_URL_PREFIX + url.toString() + ResourceUtils.JAR_URL_SEPARATOR);
349+
if (jarResource.exists()) {
350+
result.add(jarResource);
351+
}
352+
}
353+
catch (MalformedURLException ex) {
354+
if (logger.isDebugEnabled()) {
355+
logger.debug("Cannot search for matching files underneath [" + url +
356+
"] because it cannot be converted to a valid 'jar:' URL: " + ex.getMessage());
357+
}
358+
}
359+
}
360+
}
361+
}
362+
catch (Exception ex) {
363+
if (logger.isDebugEnabled()) {
364+
logger.debug("Cannot introspect jar files since ClassLoader [" + classLoader +
365+
"] does not support 'getURLs()': " + ex);
366+
}
367+
}
368+
}
369+
if (classLoader != null) {
370+
try {
371+
addAllClassLoaderJarRoots(classLoader.getParent(), result);
372+
}
373+
catch (Exception ex) {
374+
if (logger.isDebugEnabled()) {
375+
logger.debug("Cannot introspect jar files in parent ClassLoader since [" + classLoader +
376+
"] does not support 'getParent()': " + ex);
377+
}
378+
}
379+
}
380+
}
381+
328382
/**
329383
* Find all resources that match the given location pattern via the
330384
* Ant-style PathMatcher. Supports resources in jar files and zip files

spring-core/src/main/java/org/springframework/util/ResourceUtils.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ public abstract class ResourceUtils {
5757
/** URL prefix for loading from the file system: "file:" */
5858
public static final String FILE_URL_PREFIX = "file:";
5959

60+
/** URL prefix for loading from the file system: "jar:" */
61+
public static final String JAR_URL_PREFIX = "jar:";
62+
6063
/** URL protocol for a file in the file system: "file" */
6164
public static final String URL_PROTOCOL_FILE = "file";
6265

@@ -78,7 +81,10 @@ public abstract class ResourceUtils {
7881
/** URL protocol for a general JBoss VFS resource: "vfs" */
7982
public static final String URL_PROTOCOL_VFS = "vfs";
8083

81-
/** Separator between JAR URL and file path within the JAR */
84+
/** File extension for a regular jar file: ".jar" */
85+
public static final String JAR_FILE_EXTENSION = ".jar";
86+
87+
/** Separator between JAR URL and file path within the JAR: "!/" */
8288
public static final String JAR_URL_SEPARATOR = "!/";
8389

8490

@@ -273,6 +279,18 @@ public static boolean isJarURL(URL url) {
273279
URL_PROTOCOL_VFSZIP.equals(protocol) || URL_PROTOCOL_WSJAR.equals(protocol));
274280
}
275281

282+
/**
283+
* Determine whether the given URL points to a jar file itself,
284+
* that is, has protocol "file" and ends with the ".jar" extension.
285+
* @param url the URL to check
286+
* @return whether the URL has been identified as a JAR file URL
287+
* @since 4.1
288+
*/
289+
public static boolean isJarFileURL(URL url) {
290+
return (URL_PROTOCOL_FILE.equals(url.getProtocol()) &&
291+
url.getPath().toLowerCase().endsWith(JAR_FILE_EXTENSION));
292+
}
293+
276294
/**
277295
* Extract the URL for the actual jar file from the given URL
278296
* (which may point to a resource in a jar file or to a jar file itself).

spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,9 +16,6 @@
1616

1717
package org.springframework.core.io.support;
1818

19-
import static org.junit.Assert.assertEquals;
20-
import static org.junit.Assert.fail;
21-
2219
import java.io.FileNotFoundException;
2320
import java.io.IOException;
2421
import java.util.ArrayList;
@@ -27,8 +24,11 @@
2724

2825
import org.junit.Ignore;
2926
import org.junit.Test;
27+
3028
import org.springframework.core.io.Resource;
3129

30+
import static org.junit.Assert.*;
31+
3232
/**
3333
* If this test case fails, uncomment diagnostics in
3434
* {@code assertProtocolAndFilenames} method.
@@ -91,13 +91,13 @@ public void testClasspathStarWithPatternOnFileSystem() throws IOException {
9191
Resource[] resources = resolver.getResources("classpath*:org/springframework/core/io/sup*/*.class");
9292
// Have to exclude Clover-generated class files here,
9393
// as we might be running as part of a Clover test run.
94-
List noCloverResources = new ArrayList();
95-
for (int i = 0; i < resources.length; i++) {
96-
if (resources[i].getFilename().indexOf("$__CLOVER_") == -1) {
97-
noCloverResources.add(resources[i]);
94+
List<Resource> noCloverResources = new ArrayList<Resource>();
95+
for (Resource resource : resources) {
96+
if (!resource.getFilename().contains("$__CLOVER_")) {
97+
noCloverResources.add(resource);
9898
}
9999
}
100-
resources = (Resource[]) noCloverResources.toArray(new Resource[noCloverResources.size()]);
100+
resources = noCloverResources.toArray(new Resource[noCloverResources.size()]);
101101
assertProtocolAndFilenames(resources, "file", CLASSES_IN_CORE_IO_SUPPORT, TEST_CLASSES_IN_CORE_IO_SUPPORT);
102102
}
103103

@@ -113,15 +113,29 @@ public void testClasspathStartWithPatternInJar() throws IOException {
113113
assertProtocolAndFilenames(resources, "jar", CLASSES_IN_COMMONSLOGGING);
114114
}
115115

116+
@Test
117+
public void testRootPatternRetrievalInJarFiles() throws IOException {
118+
Resource[] resources = resolver.getResources("classpath*:*.dtd");
119+
boolean found = false;
120+
for (Resource resource : resources) {
121+
if (resource.getFilename().equals("aspectj_1_5_0.dtd")) {
122+
found = true;
123+
}
124+
}
125+
assertTrue("Could not find aspectj_1_5_0.dtd in the root of the aspectjweaver jar", found);
126+
}
127+
128+
116129
private void assertProtocolAndFilename(Resource resource, String urlProtocol, String fileName) throws IOException {
117130
assertProtocolAndFilenames(new Resource[] {resource}, urlProtocol, new String[] {fileName});
118131
}
119132

120133
private void assertProtocolAndFilenames(
121134
Resource[] resources, String urlProtocol, String[] fileNames1, String[] fileNames2) throws IOException {
122-
List fileNames = new ArrayList(Arrays.asList(fileNames1));
135+
136+
List<String> fileNames = new ArrayList<String>(Arrays.asList(fileNames1));
123137
fileNames.addAll(Arrays.asList(fileNames2));
124-
assertProtocolAndFilenames(resources, urlProtocol, (String[]) fileNames.toArray(new String[fileNames.size()]));
138+
assertProtocolAndFilenames(resources, urlProtocol, fileNames.toArray(new String[fileNames.size()]));
125139
}
126140

127141
private void assertProtocolAndFilenames(Resource[] resources, String urlProtocol, String[] fileNames)
@@ -146,16 +160,15 @@ private void assertProtocolAndFilenames(Resource[] resources, String urlProtocol
146160
// }
147161

148162
assertEquals("Correct number of files found", fileNames.length, resources.length);
149-
for (int i = 0; i < resources.length; i++) {
150-
Resource resource = resources[i];
163+
for (Resource resource : resources) {
151164
assertEquals(urlProtocol, resource.getURL().getProtocol());
152165
assertFilenameIn(resource, fileNames);
153166
}
154167
}
155168

156169
private void assertFilenameIn(Resource resource, String[] fileNames) {
157-
for (int i = 0; i < fileNames.length; i++) {
158-
if (resource.getFilename().endsWith(fileNames[i])) {
170+
for (String fileName : fileNames) {
171+
if (resource.getFilename().endsWith(fileName)) {
159172
return;
160173
}
161174
}

0 commit comments

Comments
 (0)