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 1 commit
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
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 {

public @Nullable String get(final String key);

public 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);
}

}
121 changes: 82 additions & 39 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 @@ -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();
}

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(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<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"));
}
}