1 package org.webjars;
2
3 import io.github.classgraph.ClassGraph;
4 import io.github.classgraph.Resource;
5 import io.github.classgraph.ResourceList;
6 import io.github.classgraph.ScanResult;
7
8 import java.io.IOException;
9 import java.net.URI;
10 import java.util.*;
11 import java.util.Map.Entry;
12 import java.util.regex.Matcher;
13 import java.util.regex.Pattern;
14
15 /**
16  * Locate WebJar assets. The class is thread safe.
17  */

18 public class WebJarAssetLocator {
19
20     /**
21      * The webjar package name.
22      */

23     public static final String WEBJARS_PACKAGE = "META-INF.resources.webjars";
24
25     /**
26      * The path to where webjar resources live.
27      */

28     public static final String WEBJARS_PATH_PREFIX = "META-INF/resources/webjars";
29
30     private static Pattern WEBJAR_EXTRACTOR_PATTERN = Pattern.compile(WEBJARS_PATH_PREFIX + "/([^/]*)/([^/]*)/(.*)$");
31
32     static class WebJarInfo {
33         final String version;
34         final String groupId;
35         final URI uri;
36         final List<String> contents;
37
38         public WebJarInfo(final String version, final String groupId, final URI uri, final List<String> contents) {
39             this.version = version;
40             this.groupId = groupId;
41             this.uri = uri;
42             this.contents = contents;
43         }
44     }
45
46     protected final Map<String, WebJarInfo> allWebJars;
47
48     protected static ResourceList webJarResources(final String webJarName, final ResourceList resources) {
49         return resources.filter(resource -> resource.getPath().startsWith(WEBJARS_PATH_PREFIX + "/" + webJarName + "/"));
50     }
51
52     protected static String webJarVersion(final String webJarName, final ResourceList resources) {
53         if (resources.isEmpty()) {
54             return null;
55         }
56         else {
57             final String aPath = resources.get(0).getPath();
58             final String prefix = WEBJARS_PATH_PREFIX + "/" + webJarName + "/";
59             if (aPath.startsWith(prefix)) {
60                 final String withoutName = aPath.substring(prefix.length());
61                 try {
62                     final String maybeVersion = withoutName.substring(0, withoutName.indexOf("/"));
63                     ResourceList withMaybeVersion = resources.filter(resource -> resource.getPath().startsWith(prefix + maybeVersion + "/"));
64
65                     if (withMaybeVersion.size() == resources.size()) {
66                         return maybeVersion;
67                     } else {
68                         return null;
69                     }
70                 }
71                 catch (Exception e) {
72                     return null;
73                 }
74             }
75             else {
76                 return null;
77             }
78         }
79     }
80
81     private static String groupId(final URI classpathElementURI) {
82         final ClassGraph classGraph = new ClassGraph().overrideClasspath(classpathElementURI).ignoreParentClassLoaders().whitelistPaths("META-INF/maven");
83         try (ScanResult scanResult = classGraph.scan()) {
84             final ResourceList maybePomProperties = scanResult.getResourcesWithLeafName("pom.properties");
85
86             String groupId = null;
87             if (maybePomProperties.size() == 1) {
88                 try {
89                     final Properties properties = new Properties();
90                     properties.load(maybePomProperties.get(0).open());
91                     maybePomProperties.get(0).close();
92                     groupId = properties.getProperty("groupId");
93                 } catch (IOException e) {
94                     // ignored
95                 }
96             }
97
98             return groupId;
99         }
100     }
101
102     protected static Map<String, WebJarInfo> findWebJars(ScanResult scanResult) {
103         Map<String, WebJarInfo> allWebJars = new HashMap<>();
104
105         for (Resource resource : scanResult.getAllResources()) {
106             final String noPrefix = resource.getPath().substring(WEBJARS_PATH_PREFIX.length() + 1);
107             final String webJarName = noPrefix.substring(0, noPrefix.indexOf("/"));
108             WebJarInfo webJarInfo = allWebJars.get(webJarName);
109             if (webJarInfo == null) {
110                 final ResourceList webJarResources = webJarResources(webJarName, scanResult.getAllResources());
111                 final String maybeWebJarVersion = webJarVersion(webJarName, webJarResources);
112                 final String maybeGroupId = groupId(resource.getClasspathElementURI());
113                 // todo: this doesn't preserve the different URIs for the resources so if for some reason the actual duplicates are different,
114                 //       then things can get strange because on resource lookup, it can resolve to a difference classpath resource
115                 //
116                 // this removes duplicates
117                 final List<String> paths = new ArrayList<>(new HashSet<>(webJarResources.getPaths()));
118                 webJarInfo = new WebJarInfo(maybeWebJarVersion, maybeGroupId, resource.getClasspathElementURI(), paths);
119                 allWebJars.put(webJarName, webJarInfo);
120             }
121         }
122
123         return allWebJars;
124     }
125
126     /**
127      * @param path The full WebJar path
128      * @return A WebJar tuple (Entry) with key = id and value = version
129      */

130     public static Entry<String, String> getWebJar(String path) {
131
132         Matcher matcher = WEBJAR_EXTRACTOR_PATTERN.matcher(path);
133         if (matcher.find()) {
134             String id = matcher.group(1);
135             String version = matcher.group(2);
136             return new AbstractMap.SimpleEntry<>(id, version);
137         } else {
138             // not a legal WebJar file format
139             return null;
140         }
141     }
142
143     private Map<String, WebJarInfo> scanForWebJars(ClassGraph classGraph) {
144         try(ScanResult scanResult = classGraph.whitelistPaths(WEBJARS_PATH_PREFIX).scan()) {
145             return findWebJars(scanResult);
146         }
147     }
148
149     public WebJarAssetLocator() {
150         allWebJars = scanForWebJars(new ClassGraph());
151     }
152
153     public WebJarAssetLocator(final ClassLoader classLoader) {
154         allWebJars = scanForWebJars(new ClassGraph().overrideClassLoaders(classLoader).ignoreParentClassLoaders());
155     }
156
157     public WebJarAssetLocator(final String... whitelistPaths) {
158         allWebJars = scanForWebJars(new ClassGraph().whitelistPaths(whitelistPaths));
159     }
160
161     public WebJarAssetLocator(final ClassLoader classLoader, final String... whitelistPaths) {
162         allWebJars = scanForWebJars(new ClassGraph().overrideClassLoaders(classLoader).ignoreParentClassLoaders().whitelistPaths(whitelistPaths));
163     }
164
165     public WebJarAssetLocator(final Map<String, WebJarInfo> allWebJars) {
166         this.allWebJars = allWebJars;
167     }
168
169     private String throwNotFoundException(final String partialPath) {
170         throw new NotFoundException(
171                 partialPath
172                         + " could not be found. Make sure you've added the corresponding WebJar and please check for typos."
173         );
174     }
175
176     private String throwMultipleMatchesException(final String partialPath, final List<String> matches) {
177         throw new MultipleMatchesException(
178                 "Multiple matches found for "
179                         + partialPath
180                         + ". Please provide a more specific path, for example by including a version number.", matches);
181     }
182
183     /**
184      * Given a distinct path within the WebJar index passed in return the full
185      * path of the resource.
186      *
187      * @param partialPath the path to return e.g. "jquery.js" or "abc/someother.js".
188      *                    This must be a distinct path within the index passed in.
189      * @return a fully qualified path to the resource.
190      */

191     public String getFullPath(final String partialPath) {
192         List<String> paths = new ArrayList<>();
193
194         for(String webJarName : allWebJars.keySet()) {
195             try {
196                 paths.add(getFullPath(webJarName, partialPath));
197             }
198             catch (NotFoundException e) {
199                 // ignored
200             }
201         }
202
203         if (paths.size() == 0) {
204             throwNotFoundException(partialPath);
205         }
206         else if (paths.size() > 1) {
207             throwMultipleMatchesException(partialPath, paths);
208         }
209
210         return paths.get(0);
211     }
212
213     /**
214      * Returns the full path of an asset within a specific WebJar
215      *
216      * @param webjar      The id of the WebJar to search
217      * @param partialPath The partial path to look for
218      * @return a fully qualified path to the resource
219      */

220     public String getFullPath(final String webjar, final String partialPath) {
221         List<String> paths = new ArrayList<>();
222
223         for (String path : allWebJars.get(webjar).contents) {
224             if (path.endsWith(partialPath)) {
225                 paths.add(path);
226             }
227         }
228
229         if (paths.size() == 0) {
230             throwNotFoundException(partialPath);
231         }
232         else if (paths.size() > 1) {
233             throwMultipleMatchesException(partialPath, paths);
234         }
235
236         return paths.get(0);
237     }
238
239     /**
240      * Returns the full path of an asset within a specific WebJar
241      *
242      * @param webJarName      The id of the WebJar to search
243      * @param exactPath   The exact path of the file within the WebJar
244      * @return a fully qualified path to the resource
245      */

246     public String getFullPathExact(final String webJarName, final String exactPath) {
247         final String maybeVersion = getWebJars().get(webJarName);
248
249         String fullPath;
250         if (maybeVersion != null) {
251             fullPath = WEBJARS_PATH_PREFIX + "/" + webJarName + "/" + maybeVersion + "/" + exactPath;
252         }
253         else {
254             fullPath = WEBJARS_PATH_PREFIX + "/" + webJarName + "/" + exactPath;
255         }
256
257         WebJarInfo webJarInfo = allWebJars.get(webJarName);
258         if ((webJarInfo != null) && (webJarInfo.contents.contains(fullPath))) {
259             return fullPath;
260         }
261
262         return null;
263     }
264
265     public Set<String> listAssets() {
266         return listAssets("");
267     }
268
269     /**
270      * List assets within a folder.
271      *
272      * @param folderPath the root path to the folder.
273      * @return a set of folder paths that match.
274      */

275     public Set<String> listAssets(final String folderPath) {
276         Set<String> assets = new HashSet<>();
277
278         final String prefix = WEBJARS_PATH_PREFIX + (!folderPath.startsWith("/") ? "/" : "") + folderPath;
279         for (final WebJarInfo webJarInfo : allWebJars.values()) {
280             for (final String path : webJarInfo.contents) {
281                 if (path.startsWith(folderPath) || path.startsWith(prefix)) {
282                     assets.add(path);
283                 }
284             }
285         }
286
287         return assets;
288     }
289
290     /**
291      * @return A map of the WebJars based on the files in the CLASSPATH where the key is the artifactId and the value is the version
292      */

293     public Map<String, String> getWebJars() {
294         Map<String, String> webJars = new HashMap<>();
295         for (String webJarName : allWebJars.keySet()) {
296             webJars.put(webJarName, allWebJars.get(webJarName).version);
297         }
298
299         return webJars;
300     }
301
302
303     /**
304      * Gets the Group ID given a fullPath
305      *
306      * @param fullPath the fullPath to the asset in a WebJar, i.e. META-INF/resources/webjars/jquery/2.1.0/jquery.js
307      * @return the Group ID for the WebJar or null if it can't be determined
308      */

309     public String groupId(final String fullPath) {
310         String groupId = null;
311
312         for (String webJarName : allWebJars.keySet()) {
313             WebJarInfo webJarInfo = allWebJars.get(webJarName);
314             if (webJarInfo.contents.contains(fullPath)) {
315                 groupId = webJarInfo.groupId;
316             }
317         }
318
319         return groupId;
320     }
321
322 }
323