diff --git a/src/main/java/org/webjars/WebJarCache.java b/src/main/java/org/webjars/WebJarCache.java new file mode 100644 index 0000000..61cc57c --- /dev/null +++ b/src/main/java/org/webjars/WebJarCache.java @@ -0,0 +1,17 @@ +package org.webjars; + + +import org.jspecify.annotations.Nullable; + +/** + * WebJar Locator Cache Interface + * Since classpath resources are essentially immutable, the WebJarsCache does not have the concept of expiry. + * Cache keys and values are Strings because that is all that is needed. + */ +public interface WebJarCache { + + public @Nullable String get(final String key); + + public void put(final String key, final String value); + +} diff --git a/src/main/java/org/webjars/WebJarCacheDefault.java b/src/main/java/org/webjars/WebJarCacheDefault.java new file mode 100644 index 0000000..3029782 --- /dev/null +++ b/src/main/java/org/webjars/WebJarCacheDefault.java @@ -0,0 +1,25 @@ +package org.webjars; + +import org.jspecify.annotations.Nullable; + +import java.util.concurrent.ConcurrentHashMap; + +public class WebJarCacheDefault implements WebJarCache { + + final ConcurrentHashMap cache; + + public WebJarCacheDefault(ConcurrentHashMap cache) { + this.cache = cache; + } + + @Override + public @Nullable String get(String key) { + return cache.get(key); + } + + @Override + public void put(String key, String value) { + cache.put(key, value); + } + +} diff --git a/src/main/java/org/webjars/WebJarVersionLocator.java b/src/main/java/org/webjars/WebJarVersionLocator.java index c7a4d04..b86a6e7 100644 --- a/src/main/java/org/webjars/WebJarVersionLocator.java +++ b/src/main/java/org/webjars/WebJarVersionLocator.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; /** @@ -17,70 +18,112 @@ public class WebJarVersionLocator { /** * The path to where webjar resources live. */ - public static final String WEBJARS_PATH_PREFIX = "META-INF/resources/webjars"; + public final String WEBJARS_PATH_PREFIX = "META-INF/resources/webjars"; - private static final String PROPERTIES_ROOT = "META-INF/maven/"; - private static final String NPM = "org.webjars.npm/"; - private static final String PLAIN = "org.webjars/"; - private static final String POM_PROPERTIES = "/pom.properties"; + private final String PROPERTIES_ROOT = "META-INF/maven/"; + private final String NPM = "org.webjars.npm/"; + private final String PLAIN = "org.webjars/"; + private final String POM_PROPERTIES = "/pom.properties"; - private static final ClassLoader LOADER = WebJarVersionLocator.class.getClassLoader(); + private final ClassLoader LOADER = WebJarVersionLocator.class.getClassLoader(); + private final WebJarCache cache; - @Nullable - public static String fullPath(final String webJarName, final String exactPath) { - String version = webJarVersion(webJarName); - String fullPath = String.format("%s/%s/%s", WEBJARS_PATH_PREFIX, webJarName, exactPath); - if (!isEmpty(version)) { - if (!exactPath.startsWith(version)) { - fullPath = String.format("%s/%s/%s/%s", WEBJARS_PATH_PREFIX, webJarName, version, exactPath); - } - } + public WebJarVersionLocator() { + this.cache = new WebJarCacheDefault(new ConcurrentHashMap<>()); + } + + public WebJarVersionLocator(WebJarCache cache) { + this.cache = cache; + } - if (LOADER.getResource(fullPath) != null) { - return fullPath; + public static class DEFAULT { + private static final WebJarVersionLocator webJarVersionLocator = new WebJarVersionLocator(); + public static final String WEBJARS_PATH_PREFIX = webJarVersionLocator.WEBJARS_PATH_PREFIX; + + @Nullable + public static String fullPath(final String webJarName, final String exactPath) { + return webJarVersionLocator.fullPath(webJarName, exactPath); } - return null; + @Nullable + public static String webJarVersion(final String webJarName) { + return webJarVersionLocator.webJarVersion(webJarName); + } } @Nullable - public static String webJarVersion(final String webJarName) { - InputStream resource = LOADER.getResourceAsStream(PROPERTIES_ROOT + NPM + webJarName + POM_PROPERTIES); - if (resource == null) { - resource = LOADER.getResourceAsStream(PROPERTIES_ROOT + PLAIN + webJarName + POM_PROPERTIES); - } + public String fullPath(final String webJarName, final String exactPath) { + final String cacheKey = "fullpath-" + webJarName + "-" + exactPath; + final String maybeCached = cache.get(cacheKey); + if (maybeCached == null) { + final String version = webJarVersion(webJarName); + String fullPath = String.format("%s/%s/%s", WEBJARS_PATH_PREFIX, webJarName, exactPath); + if (!isEmpty(version)) { + if (!exactPath.startsWith(version)) { + fullPath = String.format("%s/%s/%s/%s", WEBJARS_PATH_PREFIX, webJarName, version, exactPath); + } + } - // Webjars also uses org.webjars.bower as a group id, but the resource paths are not as standard (and not so many people use those) - if (resource != null) { - Properties properties = new Properties(); - try { - properties.load(resource); - } catch (IOException ignored) { + if (LOADER.getResource(fullPath) != null) { + cache.put(cacheKey, fullPath); + return fullPath; + } + + return null; + } + else { + return maybeCached; + } + } + @Nullable + public String webJarVersion(final String webJarName) { + final String cacheKey = "version-" + webJarName; + final String maybeCached = cache.get(cacheKey); + if (maybeCached == null) { + InputStream resource = LOADER.getResourceAsStream(PROPERTIES_ROOT + NPM + webJarName + POM_PROPERTIES); + if (resource == null) { + resource = LOADER.getResourceAsStream(PROPERTIES_ROOT + PLAIN + webJarName + POM_PROPERTIES); } - String version = properties.getProperty("version"); - // Sometimes a webjar version is not the same as the Maven artifact version - if (version != null) { - if (hasResourcePath(webJarName, version)) { - return version; + + // Webjars also uses org.webjars.bower as a group id, but the resource paths are not as standard (and not so many people use those) + if (resource != null) { + final Properties properties = new Properties(); + try { + properties.load(resource); + } catch (IOException ignored) { + } - if (version.contains("-")) { - version = version.substring(0, version.indexOf("-")); + String version = properties.getProperty("version"); + // Sometimes a webjar version is not the same as the Maven artifact version + if (version != null) { if (hasResourcePath(webJarName, version)) { + cache.put(cacheKey, version); return version; } + if (version.contains("-")) { + version = version.substring(0, version.indexOf("-")); + if (hasResourcePath(webJarName, version)) { + cache.put(cacheKey, version); + return version; + } + } } } + + return null; + } + else { + return maybeCached; } - return null; } - private static boolean hasResourcePath(final String webJarName, final String path) { + private boolean hasResourcePath(final String webJarName, final String path) { return LOADER.getResource(WEBJARS_PATH_PREFIX + "/" + webJarName + "/" + path) != null; } - private static boolean isEmpty(final String str) { + private boolean isEmpty(final String str) { return str == null || str.trim().isEmpty(); } diff --git a/src/test/java/org/webjars/WebJarVersionLocatorTest.java b/src/test/java/org/webjars/WebJarVersionLocatorTest.java index 9835166..1fce766 100644 --- a/src/test/java/org/webjars/WebJarVersionLocatorTest.java +++ b/src/test/java/org/webjars/WebJarVersionLocatorTest.java @@ -5,30 +5,50 @@ import org.junit.Test; +import java.util.concurrent.ConcurrentHashMap; + public class WebJarVersionLocatorTest { @Test public void invalid_webjar_path_should_return_null() { - assertNull(WebJarVersionLocator.webJarVersion("foo")); + assertNull(WebJarVersionLocator.DEFAULT.webJarVersion("foo")); } @Test public void should_get_a_webjar_version() { - assertEquals("3.1.1", WebJarVersionLocator.webJarVersion("bootswatch-yeti")); + assertEquals("3.1.1", WebJarVersionLocator.DEFAULT.webJarVersion("bootswatch-yeti")); } @Test public void webjar_version_doesnt_match_path() { - assertEquals("3.1.1", WebJarVersionLocator.webJarVersion("bootstrap")); + assertEquals("3.1.1", WebJarVersionLocator.DEFAULT.webJarVersion("bootstrap")); } @Test public void full_path_exists_version_not_supplied() { - assertEquals(WebJarVersionLocator.WEBJARS_PATH_PREFIX + "/bootstrap/3.1.1/js/bootstrap.js", WebJarVersionLocator.fullPath("bootstrap", "js/bootstrap.js")); + assertEquals(WebJarVersionLocator.DEFAULT.WEBJARS_PATH_PREFIX + "/bootstrap/3.1.1/js/bootstrap.js", WebJarVersionLocator.DEFAULT.fullPath("bootstrap", "js/bootstrap.js")); } @Test public void full_path_exists_version_supplied() { - assertEquals(WebJarVersionLocator.WEBJARS_PATH_PREFIX + "/bootstrap/3.1.1/js/bootstrap.js", WebJarVersionLocator.fullPath("bootstrap", "3.1.1/js/bootstrap.js")); + assertEquals(WebJarVersionLocator.DEFAULT.WEBJARS_PATH_PREFIX + "/bootstrap/3.1.1/js/bootstrap.js", WebJarVersionLocator.DEFAULT.fullPath("bootstrap", "3.1.1/js/bootstrap.js")); + } + + @Test + public void cache_is_populated_on_lookup() { + final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + final WebJarVersionLocator webJarVersionLocator = new WebJarVersionLocator(new WebJarCacheDefault(cache)); + + assertEquals("3.1.1", webJarVersionLocator.webJarVersion("bootstrap")); + assertEquals(1, cache.size()); + // should hit the cache and produce the same value + // todo: test that it was actually a cache hit + assertEquals("3.1.1", webJarVersionLocator.webJarVersion("bootstrap")); + + assertEquals(webJarVersionLocator.WEBJARS_PATH_PREFIX + "/bootstrap/3.1.1/js/bootstrap.js", webJarVersionLocator.fullPath("bootstrap", "js/bootstrap.js")); + assertEquals(2, cache.size()); + // should hit the cache and produce the same value + // todo: test that it was actually a cache hit + assertEquals(webJarVersionLocator.WEBJARS_PATH_PREFIX + "/bootstrap/3.1.1/js/bootstrap.js", webJarVersionLocator.fullPath("bootstrap", "js/bootstrap.js")); } }