/* * #%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.base.Strings; import com.google.common.collect.ImmutableList; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.TrueFileFilter; import org.apache.felix.ipojo.annotations.*; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.net.URL; import java.util.*; /** * A controller publishing the resources found in a folder and in bundles. */ @Component(immediate = true) @Provides public class AssetController extends DefaultController implements AssetProvider { public static final Logger LOGGER = LoggerFactory.getLogger(AssetController.class); /** * The default instance handle the `assets` folder. */ private final File directory; @Context private BundleContext context; private final boolean manageAssetsFromBundles; private final String pathInBundles; private final String root; @Requires ApplicationConfiguration configuration; @Requires Crypto crypto; /** * Constructor used for testing purpose only. * * @param configuration the configuration service * @param crypto the crypto service * @param bc the bundle context * @param path the external FS path * @param manageAssetsFromBundles whether or not it should handle embedded assets * @param pathInBundles the path in the bundle if enabled * @param url the root url where assets are served. */ public AssetController( ApplicationConfiguration configuration, Crypto crypto, BundleContext bc, String path, boolean manageAssetsFromBundles, String pathInBundles, String url) { this.configuration = configuration; this.crypto = crypto; this.context = bc; if (!Strings.isNullOrEmpty(path)) { this.directory = new File(configuration.getBaseDir(), path); //NOSONAR - injected service. } else { this.directory = null; } this.manageAssetsFromBundles = manageAssetsFromBundles; this.pathInBundles = computePathInBundle(pathInBundles); this.root = computeRoot(url); } /** * Creates an instance of the asset controller. This constructor is used by iPOJO. * * @param path the path of the directory containing external asset. * @param manageAssetsFromBundles do we handle the assets contained in bundles * @param pathInBundles the path in the bundle * @param url the root url */ public AssetController(@Property(name = "path", value = "") String path, @Property(name = "manageAssetsFromBundles", value = "false") boolean manageAssetsFromBundles, @Property(name = "pathInBundles", value = "/assets/") String pathInBundles, @Property(name = "url", value = "/assets") String url) { if (!Strings.isNullOrEmpty(path)) { this.directory = new File(configuration.getBaseDir(), path); //NOSONAR - injected service. } else { this.directory = null; } this.manageAssetsFromBundles = manageAssetsFromBundles; this.pathInBundles = computePathInBundle(pathInBundles); this.root = computeRoot(url); if (manageAssetsFromBundles) { LOGGER.info("Serving assets from bundles ({}) on {}", pathInBundles, root); } LOGGER.info("Serving assets from file system ({}) on {}", path, root); } private String computeRoot(String url) { if (url != null) { if (!url.startsWith("/")) { throw new IllegalArgumentException("The `url` property must start with `/`"); } return url; } else { return "/assets"; } } protected String computePathInBundle(String pathInBundles) { if (manageAssetsFromBundles && !Strings.isNullOrEmpty(pathInBundles)) { if (!pathInBundles.startsWith("/") || !pathInBundles.endsWith("/")) { throw new IllegalArgumentException("The `pathInBundles` property must start and end with `/`"); } return pathInBundles; } else { return "/assets/"; } } /** * @return the 'serve' routes. */ @Override public List<Route> routes() { return ImmutableList.of(new RouteBuilder() .route(HttpMethod.GET) .on(root + "/{path+}") .to(this, "serve")); } /** * @return the result serving the asset. */ public Result serve() { String path = context().parameterFromPath("path"); if (path.startsWith("/")) { path = path.substring(1); } Asset<?> asset = getAssetFromFS(path); if (asset == null && manageAssetsFromBundles) { asset = getAssetFromBundle(path); } if (asset != null) { return CacheUtils.fromAsset(context(), asset, configuration); } return notFound(); } private Asset<URL> getAssetFromBundle(String path) { Bundle[] bundles = context.getBundles(); // Skip bundle 0 as it cannot contain assets for (int i = 1; i < bundles.length; i++) { URL url = bundles[i].getResource(pathInBundles + path); if (url != null) { return new DefaultAsset<>(root + "/" + path, url, bundles[i].getSymbolicName(), bundles[i].getLastModified(), CacheUtils.computeEtag(bundles[i].getLastModified(), configuration, crypto)); } } return null; // Asset not found, just returning null. } private Asset<File> getAssetFromFS(String path) { if (directory == null) { return null; } File file = new File(directory, path); if (!file.exists()) { return null; } return new DefaultAsset<>(root + "/" + path, file, file.getAbsolutePath(), file.lastModified(), CacheUtils.computeEtag(file.lastModified(), configuration, crypto)); } /** * @return the list of provided assets. */ @Override public Collection<Asset<?>> assets() { Map<String, Asset<?>> map = new HashMap<>(); if (directory != null && directory.isDirectory()) { // First insert the FS assets // For this iterate over the file present on the file system. Collection<File> files = FileUtils.listFiles(directory, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE); for (File file : files) { if (file.getName().startsWith(".")) { // Skip file starting with . - there are hidden. continue; } if (file.getName().startsWith(".")) { // Skip file starting with . - there are hidden. continue; } // The path is computed as follows: // root (ending with /) and the path of the file relative to the directory. As these path may contain // \ on Windows we replace them by /. String path = root + file.getAbsolutePath().substring(directory.getAbsolutePath().length()).replace("\\", "/"); // TODO Do we really need computing the ETAG here ? map.put(path, new DefaultAsset<>(path, file, file.getAbsolutePath(), file.lastModified(), null)); } } if (!manageAssetsFromBundles) { return map.values(); } // No add the bundle things. Bundle[] bundles = context.getBundles(); // Skip bundle 0 for (int i = 1; i < bundles.length; i++) { // Remove the last "/" - we are sure to have one. URL root = bundles[i].getEntry(pathInBundles.substring(0, pathInBundles.length() - 1)); Enumeration<URL> urls = bundles[i].findEntries(pathInBundles, "*", true); if (urls != null) { while (urls.hasMoreElements()) { URL url = urls.nextElement(); String path = url.toExternalForm().substring(root.toExternalForm().length()); if (path.startsWith("/")) { path = this.root + path; } else { path = this.root + "/" + path; } if (!map.containsKey(path)) { // We should not replace assets overridden by files. map.put(path, new DefaultAsset<>(path, url, url.toExternalForm(), bundles[i].getLastModified(), null)); } } } } return map.values(); } /** * 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) { Asset<?> asset = getAssetFromFS(path); if (asset == null) { asset = getAssetFromBundle(path); } return asset; } }