package io.dropwizard.bundles.assets; import com.google.common.base.Charsets; import com.google.common.cache.CacheBuilderSpec; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import io.dropwizard.ConfiguredBundle; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.google.common.base.Preconditions.checkArgument; /** * An assets bundle (like io.dropwizard.assets.AssetsBundle) that utilizes configuration to provide * the ability to override how assets are loaded and cached. Specifying an override is useful * during the development phase to allow assets to be loaded directly out of source directories * instead of the classpath and to force them to not be cached by the browser or the server. This * allows developers to edit an asset, save and then immediately refresh the web browser and see the * updated assets. No compilation or copy steps are necessary. */ public class ConfiguredAssetsBundle implements ConfiguredBundle<AssetsBundleConfiguration> { private static final Logger LOGGER = LoggerFactory.getLogger(ConfiguredAssetsBundle.class); private static final String DEFAULT_PATH = "/assets"; public static final CacheBuilderSpec DEFAULT_CACHE_SPEC = CacheBuilderSpec.parse("maximumSize=100"); private static final String DEFAULT_INDEX_FILE = "index.htm"; private static final String DEFAULT_SERVLET_MAPPING_NAME = "assets"; private final Iterable<Map.Entry<String, String>> resourcePathToUriMappings; private final CacheBuilderSpec cacheBuilderSpec; private final String indexFile; private final String assetsName; /** * Creates a new {@link ConfiguredAssetsBundle} which serves up static assets from * {@code src/main/resources/assets/*} as {@code /assets/*}. * * @see ConfiguredAssetsBundle#ConfiguredAssetsBundle(String, String, String, String, * CacheBuilderSpec) */ public ConfiguredAssetsBundle() { this(DEFAULT_PATH); } /** * Creates a new {@link ConfiguredAssetsBundle} which will configure the service to serve the * static files located in {@code src/main/resources/${path}} as {@code /${path}}. For example, * given a {@code path} of {@code "/assets"}, {@code src/main/resources/assets/example.js} would * be served up from {@code /assets/example.js}. * * @param path the classpath and URI root of the static asset files * @see ConfiguredAssetsBundle#ConfiguredAssetsBundle(String, String, String, String, * CacheBuilderSpec) */ public ConfiguredAssetsBundle(String path) { this(path, path, DEFAULT_INDEX_FILE, DEFAULT_SERVLET_MAPPING_NAME, DEFAULT_CACHE_SPEC); } /** * Creates a new {@link ConfiguredAssetsBundle} which will configure the service to serve the * static files located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. For * example, given a {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"}, * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}. * * @param resourcePath the resource path (in the classpath) of the static asset files * @param uriPath the uri path for the static asset files * @see ConfiguredAssetsBundle#ConfiguredAssetsBundle(String, String, String, String, * CacheBuilderSpec) */ public ConfiguredAssetsBundle(String resourcePath, String uriPath) { this(resourcePath, uriPath, DEFAULT_INDEX_FILE, DEFAULT_SERVLET_MAPPING_NAME, DEFAULT_CACHE_SPEC); } /** * Creates a new AssetsBundle which will configure the service to serve the static files * located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. If no file name * is in ${uriPath}, ${indexFile} is appended before serving. For example, given a * {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"}, * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}. * * @param resourcePath the resource path (in the classpath) of the static asset files * @param uriPath the uri path for the static asset files * @param indexFile the name of the index file to use */ public ConfiguredAssetsBundle(String resourcePath, String uriPath, String indexFile) { this(resourcePath, uriPath, indexFile, DEFAULT_SERVLET_MAPPING_NAME, DEFAULT_CACHE_SPEC); } /** * Creates a new AssetsBundle which will configure the service to serve the static files * located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. If no file name * is in ${uriPath}, ${indexFile} is appended before serving. For example, given a * {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"}, * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}. * * @param resourcePath the resource path (in the classpath) of the static asset files * @param uriPath the uri path for the static asset files * @param indexFile the name of the index file to use * @param assetsName the name of servlet mapping used for this assets bundle */ public ConfiguredAssetsBundle(String resourcePath, String uriPath, String indexFile, String assetsName) { this(resourcePath, uriPath, indexFile, assetsName, DEFAULT_CACHE_SPEC); } /** * Creates a new {@link ConfiguredAssetsBundle} which serves up static assets from * {@code src/main/resources/assets/*} as {@code /assets/*}. * * @param cacheBuilderSpec the spec for the cache builder * @see ConfiguredAssetsBundle#ConfiguredAssetsBundle(Map, String, String, CacheBuilderSpec) */ public ConfiguredAssetsBundle(CacheBuilderSpec cacheBuilderSpec) { this(DEFAULT_PATH, DEFAULT_PATH, DEFAULT_INDEX_FILE, DEFAULT_SERVLET_MAPPING_NAME, cacheBuilderSpec); } /** * Creates a new {@link ConfiguredAssetsBundle} which will configure the service to serve the * static files located in {@code src/main/resources/${path}} as {@code /${path}}. For example, * given a {@code path} of {@code "/assets"}, {@code src/main/resources/assets/example.js} would * be served up from {@code /assets/example.js}. * * @param resourcePath the resource path (in the classpath) of the static asset files * @param cacheBuilderSpec the spec for the cache builder */ public ConfiguredAssetsBundle(String resourcePath, CacheBuilderSpec cacheBuilderSpec) { this(resourcePath, resourcePath, DEFAULT_INDEX_FILE, DEFAULT_SERVLET_MAPPING_NAME, cacheBuilderSpec); } /** * Creates a new {@link ConfiguredAssetsBundle} which will configure the service to serve the * static files located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. For * example, given a {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"}, * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}. * * @param resourcePath the resource path (in the classpath) of the static asset files * @param uriPath the uri path for the static asset files * @param cacheBuilderSpec the spec for the cache builder * @see ConfiguredAssetsBundle#ConfiguredAssetsBundle(Map, String, String, CacheBuilderSpec) */ public ConfiguredAssetsBundle(String resourcePath, String uriPath, CacheBuilderSpec cacheBuilderSpec) { this(resourcePath, uriPath, DEFAULT_INDEX_FILE, DEFAULT_SERVLET_MAPPING_NAME, cacheBuilderSpec); } /** * Creates a new AssetsBundle which will configure the service to serve the static files * located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. If no file name * is in ${uriPath}, ${indexFile} is appended before serving. For example, given a * {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"}, * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}. * * @param resourcePath the resource path (in the classpath) of the static asset files * @param uriPath the uri path for the static asset files * @param indexFile the name of the index file to use * @param cacheBuilderSpec the spec for the cache builder * @see ConfiguredAssetsBundle#ConfiguredAssetsBundle(Map, String, String, CacheBuilderSpec) */ public ConfiguredAssetsBundle(String resourcePath, String uriPath, String indexFile, CacheBuilderSpec cacheBuilderSpec) { this(resourcePath, uriPath, indexFile, DEFAULT_SERVLET_MAPPING_NAME, cacheBuilderSpec); } /** * Creates a new {@link ConfiguredAssetsBundle} which will configure the service to serve the * static files located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. For * example, given a {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"}, * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}. * * @param resourcePath the resource path (in the classpath) of the static asset files * @param cacheBuilderSpec the spec for the cache builder * @param uriPath the uri path for the static asset files * @param indexFile the name of the index file to use * @param assetsName the name of servlet mapping used for this assets bundle */ public ConfiguredAssetsBundle(String resourcePath, String uriPath, String indexFile, String assetsName, CacheBuilderSpec cacheBuilderSpec) { this(ImmutableMap.<String, String>builder().put(resourcePath, uriPath).build(), indexFile, assetsName, cacheBuilderSpec); } /** * Creates a new {@link ConfiguredAssetsBundle} which will configure the service to serve the * static files located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. For * example, given a {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"}, * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}. * * @param resourcePathToUriMappings a series of mappings from resource paths (in the classpath) * to the uri path that hosts the resource * @see ConfiguredAssetsBundle#ConfiguredAssetsBundle(String, String, String, String, * CacheBuilderSpec) */ public ConfiguredAssetsBundle(Map<String, String> resourcePathToUriMappings) { this(resourcePathToUriMappings, DEFAULT_INDEX_FILE, DEFAULT_SERVLET_MAPPING_NAME, DEFAULT_CACHE_SPEC); } /** * Creates a new AssetsBundle which will configure the service to serve the static files * located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. If no file name * is in ${uriPath}, ${indexFile} is appended before serving. For example, given a * {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"}, * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}. * * @param resourcePathToUriMappings a series of mappings from resource paths (in the classpath) * to the uri path that hosts the resource * @param indexFile the name of the index file to use */ public ConfiguredAssetsBundle(Map<String, String> resourcePathToUriMappings, String indexFile) { this(resourcePathToUriMappings, indexFile, DEFAULT_SERVLET_MAPPING_NAME, DEFAULT_CACHE_SPEC); } /** * Creates a new AssetsBundle which will configure the service to serve the static files * located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. If no file name * is in ${uriPath}, ${indexFile} is appended before serving. For example, given a * {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"}, * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}. * * @param resourcePathToUriMappings a series of mappings from resource paths (in the classpath) * to the uri path that hosts the resource * @param indexFile the name of the index file to use * @param assetsName the name of servlet mapping used for this assets bundle */ public ConfiguredAssetsBundle(Map<String, String> resourcePathToUriMappings, String indexFile, String assetsName) { this(resourcePathToUriMappings, indexFile, assetsName, DEFAULT_CACHE_SPEC); } /** * Creates a new {@link ConfiguredAssetsBundle} which will configure the service to serve the * static files located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. For * example, given a {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"}, * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}. * * @param resourcePathToUriMappings a series of mappings from resource paths (in the classpath) * to the uri path that hosts the resource * @param cacheBuilderSpec the spec for the cache builder * @see ConfiguredAssetsBundle#ConfiguredAssetsBundle(Map, String, String, CacheBuilderSpec) */ public ConfiguredAssetsBundle(Map<String, String> resourcePathToUriMappings, CacheBuilderSpec cacheBuilderSpec) { this(resourcePathToUriMappings, DEFAULT_INDEX_FILE, DEFAULT_SERVLET_MAPPING_NAME, cacheBuilderSpec); } /** * Creates a new AssetsBundle which will configure the service to serve the static files * located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. If no file name * is in ${uriPath}, ${indexFile} is appended before serving. For example, given a * {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"}, * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}. * * @param resourcePathToUriMappings a series of mappings from resource paths (in the classpath) * to the uri path that hosts the resource * @param indexFile the name of the index file to use * @param cacheBuilderSpec the spec for the cache builder * @see ConfiguredAssetsBundle#ConfiguredAssetsBundle(Map, String, String, * CacheBuilderSpec) */ public ConfiguredAssetsBundle(Map<String, String> resourcePathToUriMappings, String indexFile, CacheBuilderSpec cacheBuilderSpec) { this(resourcePathToUriMappings, indexFile, DEFAULT_SERVLET_MAPPING_NAME, cacheBuilderSpec); } /** * Creates a new {@link ConfiguredAssetsBundle} which will configure the service to serve the * static files located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. For * example, given a {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"}, * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}. * * @param resourcePathToUriMappings a series of mappings from resource paths (in the classpath) * to the uri path that hosts the resource * @param cacheBuilderSpec the spec for the cache builder * @param indexFile the name of the index file to use * @param assetsName the name of servlet mapping used for this assets bundle */ public ConfiguredAssetsBundle(Map<String, String> resourcePathToUriMappings, String indexFile, String assetsName, CacheBuilderSpec cacheBuilderSpec) { for (Map.Entry<String, String> mapping : resourcePathToUriMappings.entrySet()) { String resourcePath = mapping.getKey(); checkArgument(resourcePath.startsWith("/"), "%s is not an absolute path", resourcePath); checkArgument(!"/".equals(resourcePath), "%s is the classpath root", resourcePath); } this.resourcePathToUriMappings = Iterables.unmodifiableIterable(resourcePathToUriMappings.entrySet()); this.cacheBuilderSpec = cacheBuilderSpec; this.indexFile = indexFile; this.assetsName = assetsName; } @Override public void initialize(Bootstrap<?> bootstrap) { // nothing to do } @Override public void run(AssetsBundleConfiguration bundleConfig, Environment env) throws Exception { AssetsConfiguration config = bundleConfig.getAssetsConfiguration(); // Let the cache spec from the configuration override the one specified in the code CacheBuilderSpec spec = (config.getCacheSpec() != null) ? CacheBuilderSpec.parse(config.getCacheSpec()) : cacheBuilderSpec; Iterable<Map.Entry<String, String>> overrides = config.getOverrides().entrySet(); Iterable<Map.Entry<String, String>> mimeTypes = config.getMimeTypes().entrySet(); Iterable<Map.Entry<String, String>> servletResourcePathToUriMappings; if (!config.getResourcePathToUriMappings().isEmpty()) { servletResourcePathToUriMappings = config.getResourcePathToUriMappings().entrySet(); } else { servletResourcePathToUriMappings = resourcePathToUriMappings; } AssetServlet servlet = new AssetServlet(servletResourcePathToUriMappings, indexFile, Charsets.UTF_8, spec, overrides, mimeTypes); for (Map.Entry<String, String> mapping : servletResourcePathToUriMappings) { String mappingPath = mapping.getValue(); if (!mappingPath.endsWith("/")) { mappingPath += '/'; } mappingPath += "*"; servlet.setCacheControlHeader(config.getCacheControlHeader()); LOGGER.info("Registering ConfiguredAssetBundle with name: {} for path {}", assetsName, mappingPath); env.servlets().addServlet(assetsName, servlet).addMapping(mappingPath); } } }