/* * #%L * Wisdom-Framework * %% * Copyright (C) 2013 - 2014 Wisdom Framework * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ package org.wisdom.resources; import com.google.common.collect.ImmutableList; import org.apache.felix.ipojo.annotations.*; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleEvent; import org.osgi.util.tracker.BundleTracker; import org.osgi.util.tracker.BundleTrackerCustomizer; import org.wisdom.api.Controller; import org.wisdom.api.DefaultController; import org.wisdom.api.asset.Asset; import org.wisdom.api.asset.AssetProvider; import org.wisdom.api.asset.DefaultAsset; import org.wisdom.api.configuration.ApplicationConfiguration; import org.wisdom.api.crypto.Crypto; import org.wisdom.api.http.HttpMethod; import org.wisdom.api.http.Result; import org.wisdom.api.router.Route; import org.wisdom.api.router.RouteBuilder; import java.io.File; import java.io.FileFilter; import java.net.URL; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A controller serving WebJars. * WebJars (http://www.webjars.org) are jar files embedding web resources. * <p> * The Wisdom Maven plugin copies these files to 'assets/libs' and from bundles.Web Jar resources are served * from: * <ol> * <li>/libs/libraryname/version/path</li> * <li>/libs/libraryname/path</li> * <li>/libs/path</li> * </ol> */ @Component(immediate = true) @Provides(specifications = {Controller.class, AssetProvider.class}) @Instantiate(name = "WebJarResourceController") public class WebJarController extends DefaultController implements BundleTrackerCustomizer<List<BundleWebJarLib>>, AssetProvider { /** * A regex checking the the given path is the root of a Web Jar Lib. */ public static final Pattern WEBJAR_ROOT_REGEX = Pattern.compile(".*META-INF/resources/webjars/([^/]+)/([^/]+)/"); /** * The path containing the web libraries in a bundle. */ public static final String WEBJAR_LOCATION = "META-INF/resources/webjars/"; /** * A regex extracting the library name and version from Zip Entry names. */ public static final Pattern WEBJAR_REGEX = Pattern.compile(".*META-INF/resources/webjars/([^/]+)/([^/]+)/.*"); /** * The RegEx pattern to identify the shape of the url. */ static Pattern PATTERN = Pattern.compile("([^/]+)(/([^/]+))?/(.*)"); /** * The instance of deployer. */ private final WebJarDeployer deployer; /** * The default instance handle the `assets/libs` folder. */ private final File directory; private final BundleTracker<List<BundleWebJarLib>> tracker; Set<WebJarLib> libraries = new TreeSet<>(new Comparator<WebJarLib>() { @Override public int compare(WebJarLib o1, WebJarLib o2) { if (o1 instanceof FileWebJarLib && o2 instanceof BundleWebJarLib) { return -1; } if (o1 instanceof BundleWebJarLib && o2 instanceof FileWebJarLib) { return 1; } return o1.toString().compareTo(o2.toString()); } }); @Requires Crypto crypto; @Requires ApplicationConfiguration configuration; /** * Constructor used for testing purpose only. * * @param crypto the crypto service * @param configuration the configuration * @param path the path (relative to the configuration's base dir) */ WebJarController(Crypto crypto, ApplicationConfiguration configuration, String path) { this.crypto = crypto; this.configuration = configuration; directory = new File(configuration.getBaseDir(), path); //NOSONAR Injected field tracker = null; deployer = null; start(); } /** * Creates the controller serving resources embedded in WebJars. * * @param context the bundle context * @param path the path (relative to the configuration's base dir) in which exploded webjars are */ public WebJarController(@Context BundleContext context, @Property(value = "assets/libs", name = "path") String path) { directory = new File(configuration.getBaseDir(), path); //NOSONAR Injected field tracker = new BundleTracker<>(context, Bundle.ACTIVE, this); deployer = new WebJarDeployer(context, this); } /** * Starts the controllers. */ @Validate public void start() { if (directory.isDirectory()) { buildFileIndex(); } if (tracker != null) { tracker.open(); } if (deployer != null) { deployer.start(); } } /** * Stops the controllers. */ @Invalidate public void stop() { if (deployer != null) { deployer.stop(); } if (tracker != null) { tracker.close(); } libraries.clear(); } private void buildFileIndex() { if (directory.listFiles() == null) { // Empty. return; } FileFilter isDirectory = new FileFilter() { @Override public boolean accept(File pathname) { return pathname.isDirectory(); } }; File[] names = directory.listFiles(isDirectory); if (names == null) { // names is null if directory does not denote a valid file. return; } // Build index from files synchronized (this) { for (File dir : names) { String library = dir.getName(); File[] versions = dir.listFiles(isDirectory); if (versions == null) { // versions is null if dir does not denote a valid file. continue; } for (File ver : versions) { String version = ver.getName(); FileWebJarLib lib = new FileWebJarLib(library, version, ver); logger().info("Exploded web jar libraries detected : {}", lib); libraries.add(lib); } } } } int indexSize() { int count = 0; for (WebJarLib lib : libs()) { count += lib.names().size(); } return count; } synchronized List<WebJarLib> libs() { return new ArrayList<>(libraries); } private List<WebJarLib> findLibsContaining(String path) { List<WebJarLib> list = new ArrayList<>(); for (WebJarLib lib : libs()) { if (lib.contains(path)) { list.add(lib); } } return list; } private List<WebJarLib> find(String name) { List<WebJarLib> list = new ArrayList<>(); for (WebJarLib lib : libs()) { if (lib.name.equals(name)) { list.add(lib); } } return list; } /** * @return the router serving the assets embedded in WebJars. */ @Override public List<Route> routes() { return ImmutableList.of( new RouteBuilder() .route(HttpMethod.GET) .on("/" + "libs" + "/{path+}") .to(this, "serve") ); } /** * @return the asset embedded in a web jar. */ public Result serve() { String path = context().parameterFromPath("path"); if (path == null) { logger().error("Cannot server Web Jar resource : no path"); return badRequest(); } Asset<?> asset = assetAt(path); if (asset == null) { return notFound(); } return CacheUtils.fromAsset(context(), asset, configuration); } private WebJarLib find(String name, String version) { for (WebJarLib lib : libs()) { if (lib.name.equals(name) && lib.version.equals(version)) { return lib; } } return null; } /** * A bundle just arrived (and / or just becomes ACTIVE). We need to check if it contains 'webjar libraries'. * * @param bundle the bundle * @param bundleEvent the event * @return the list of webjar found in the bundle, empty if none. */ @Override public synchronized List<BundleWebJarLib> addingBundle(Bundle bundle, BundleEvent bundleEvent) { Enumeration<URL> e = bundle.findEntries(WEBJAR_LOCATION, "*", true); if (e == null) { // No match return Collections.emptyList(); } List<BundleWebJarLib> list = new ArrayList<>(); while (e.hasMoreElements()) { String path = e.nextElement().getPath(); if (path.endsWith("/")) { Matcher matcher = WEBJAR_ROOT_REGEX.matcher(path); if (matcher.matches()) { String name = matcher.group(1); String version = matcher.group(2); final BundleWebJarLib lib = new BundleWebJarLib(name, version, bundle); logger().info("Web Jar library ({}) found in {} [{}]", lib, bundle.getSymbolicName(), bundle.getBundleId()); list.add(lib); } } } addWebJarLibs(list); return list; } /** * Adds the given set of {@link WebJarLib} to the managed libraries. * @param list the set to add */ public void addWebJarLibs(Collection<? extends WebJarLib> list) { synchronized (this) { libraries.addAll(list); } } /** * A bundle is updated. * * @param bundle the bundle * @param bundleEvent the event * @param webJarLibs the webjars that were embedded in the previous version of the bundle. */ @Override public void modifiedBundle(Bundle bundle, BundleEvent bundleEvent, List<BundleWebJarLib> webJarLibs) { // Remove all WebJars from the given bundle, and then read tem. synchronized (this) { removedBundle(bundle, bundleEvent, webJarLibs); addingBundle(bundle, bundleEvent); } } /** * A bundle is removed. * * @param bundle the bundle * @param bundleEvent the event * @param webJarLibs the webjars that were embedded in the bundle. */ @Override public void removedBundle(Bundle bundle, BundleEvent bundleEvent, List<BundleWebJarLib> webJarLibs) { removeWebJarLibs(webJarLibs); } public void removeWebJarLibs(Collection<? extends WebJarLib> webJarLibs) { synchronized (this) { libraries.removeAll(webJarLibs); } } /** * @return the list of provided assets. */ @Override public Collection<Asset<?>> assets() { List<Asset<?>> assets = new ArrayList<>(); for (WebJarLib lib : libraries) { for (String path : lib.names()) { if (path.endsWith("/") || path.startsWith(".")) { continue; } String url = "/libs/" + lib.name + "/" + lib.version + "/" + path; DefaultAsset<?> asset = new DefaultAsset<>(url, lib.get(path), lib.toString(), lib.lastModified(), null); assets.add(asset); } } return assets; } /** * Retrieves an asset. * * @param path the asset path * @return the Asset object, or {@literal null} if the current provider can't serve this asset. */ @Override public Asset<?> assetAt(String path) { List<WebJarLib> candidates = findLibsContaining(path); if (candidates.size() == 1) { // Perfect ! only one match return new DefaultAsset<>( "/libs/" + candidates.get(0).name + "/" + candidates.get(0).version + "/" + path, candidates.get(0).get(path), candidates.get(0).toString(), candidates.get(0).lastModified(), CacheUtils.computeEtag(candidates.get(0).lastModified(), configuration, crypto) ); } else if (candidates.size() > 1) { // Several candidates logger().warn("{} WebJars provide '{}' - returning the one from {}-{}", candidates.size(), path, candidates.get(0).name, candidates.get(0).version); return new DefaultAsset<>( "/libs/" + candidates.get(0).name + "/" + candidates.get(0).version + "/" + path, candidates.get(0).get(path), candidates.get(0).toString(), candidates.get(0).lastModified(), CacheUtils.computeEtag(candidates.get(0).lastModified(), configuration, crypto) ); } else { Matcher matcher = PATTERN.matcher(path); if (!matcher.matches()) { // It should have been handled by the path match. return null; } final String name = matcher.group(1); final String version = matcher.group(3); if (version != null) { String rel = matcher.group(4); // We have a name and a version // Try to find the matching library WebJarLib lib = find(name, version); if (lib != null) { return new DefaultAsset<>( rel, lib.get(rel), lib.toString(), lib.lastModified(), CacheUtils.computeEtag(lib.lastModified(), configuration, crypto) ); } // If not found, it may be because the version is not really the version but a segment of the path. } // If we reach this point it means that the name/version lookup has failed, try without the version String rel = matcher.group(4); if (version != null) { // We have a group 3 rel = version + "/" + rel; } List<WebJarLib> libs = find(name); if (libs.size() == 1) { // Only on library has the given name if (libs.get(0).contains(rel)) { WebJarLib lib = libs.get(0); return new DefaultAsset<>( "/libs/" + lib.name + "/" + lib.version + "/" + rel, lib.get(rel), lib.toString(), lib.lastModified(), CacheUtils.computeEtag(lib.lastModified(), configuration, crypto) ); } } else if (libs.size() > 1) { // Several candidates WebJarLib higher = null; ComparableVersion higherVersion = null; for (WebJarLib lib: libs) { if (lib.contains(rel)) { if(higher == null) { higher = lib; higherVersion = new ComparableVersion(higher.version); } else { ComparableVersion newVersion = new ComparableVersion(lib.version); if(newVersion.compareTo(higherVersion) > 0) { higher = lib; higherVersion = new ComparableVersion(higher.version); } } } } if(higher != null) { logger().warn("{} WebJars match the request '{}' - returning the resource from {}-{}", libs.size(), path, higher.name, higher.version); return new DefaultAsset<>( "/libs/" + higher.name + "/" + higher.version + "/" + rel, higher.get(rel), higher.toString(), higher.lastModified(), CacheUtils.computeEtag(higher.lastModified(), configuration, crypto) ); } } return null; } } }