Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add cache #4

Merged
merged 8 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,26 @@ Usage

> Get the version of a WebJar on the classpath
```
WebJarVersionLocator.webJarVersion("bootstrap")
new WebJarVersionLocator().webJarVersion("bootstrap");
```

> Get the full path to a file in a WebJar
```
WebJarVersionLocator.fullPath("bootstrap", "js/bootstrap.js")
new WebJarVersionLocator().fullPath("bootstrap", "js/bootstrap.js");
```

`WebJarVersionLocator` has a built-in threadsafe cache that is created on construction. It is highly recommended that you use it as a Singleton to utilize the cache, i.e.
```
WebJarVersionLocator webJarVersionLocator = new WebJarVersionLocator();
webJarVersionLocator("bootstrap"); // cache miss
webJarVersionLocator("bootstrap"); // cache hit, avoiding looking up metadata in the classpath
```

The default cache uses a `ConcurrentHashMap` but you can provide a custom cache implementation:
```
class WebJarCacheMine implements WebJarCache {
...
}

WebJarVersionLocator webJarVersionLocator = new WebJarVersionLocator(new WebJarCacheMine());
```
17 changes: 17 additions & 0 deletions src/main/java/org/webjars/WebJarCache.java
Original file line number Diff line number Diff line change
@@ -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 {

@Nullable String get(final String key);

void put(final String key, final String value);

}
25 changes: 25 additions & 0 deletions src/main/java/org/webjars/WebJarCacheDefault.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.webjars;

import org.jspecify.annotations.Nullable;

import java.util.concurrent.ConcurrentHashMap;

public class WebJarCacheDefault implements WebJarCache {

final ConcurrentHashMap<String, String> cache;

public WebJarCacheDefault(ConcurrentHashMap<String, String> 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);
}

}
111 changes: 74 additions & 37 deletions src/main/java/org/webjars/WebJarVersionLocator.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;


/**
Expand All @@ -20,67 +21,103 @@ public class WebJarVersionLocator {
public static 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 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;

public WebJarVersionLocator() {
this.cache = new WebJarCacheDefault(new ConcurrentHashMap<>());
}

public WebJarVersionLocator(WebJarCache cache) {
this.cache = cache;
}

/**
* @param webJarName The name of the WebJar, i.e. bootstrap
* @param exactPath The path to the file within the WebJar, i.e. js/bootstrap.js
* @return The full path to the file in the classpath including the version, i.e. META-INF/resources/webjars/bootstrap/3.1.1/js/bootstrap.js
*/
@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 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);
}
}
}

if (LOADER.getResource(fullPath) != null) {
return fullPath;
}
if (LOADER.getResource(fullPath) != null) {
cache.put(cacheKey, fullPath);
return fullPath;
}

return null;
return null;
}
else {
return maybeCached;
}
}

/**
* @param webJarName The name of the WebJar, i.e. bootstrap
* @return The version of the WebJar, i.e 3.1.1
*/
@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 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);
}

// 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) {
// 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) {

}
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;
}
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(@Nullable final String str) {
return str == null || str.trim().isEmpty();
}

Expand Down
30 changes: 25 additions & 5 deletions src/test/java/org/webjars/WebJarVersionLocatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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(new WebJarVersionLocator().webJarVersion("foo"));
}

@Test
public void should_get_a_webjar_version() {
assertEquals("3.1.1", WebJarVersionLocator.webJarVersion("bootswatch-yeti"));
assertEquals("3.1.1", new WebJarVersionLocator().webJarVersion("bootswatch-yeti"));
}

@Test
public void webjar_version_doesnt_match_path() {
assertEquals("3.1.1", WebJarVersionLocator.webJarVersion("bootstrap"));
assertEquals("3.1.1", new WebJarVersionLocator().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.WEBJARS_PATH_PREFIX + "/bootstrap/3.1.1/js/bootstrap.js", new WebJarVersionLocator().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.WEBJARS_PATH_PREFIX + "/bootstrap/3.1.1/js/bootstrap.js", new WebJarVersionLocator().fullPath("bootstrap", "3.1.1/js/bootstrap.js"));
}

@Test
public void cache_is_populated_on_lookup() {
final ConcurrentHashMap<String, String> 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"));
}
}