001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.assets;
019
020
021
022import io.stallion.Context;
023import io.stallion.assetBundling.BundleRegistry;
024import io.stallion.exceptions.ClientException;
025import io.stallion.exceptions.UsageException;
026import io.stallion.fileSystem.FileSystemWatcherService;
027import io.stallion.services.Log;
028import io.stallion.services.PermaCache;
029import io.stallion.settings.Settings;
030import io.stallion.utils.GeneralUtils;
031import org.apache.commons.codec.digest.DigestUtils;
032
033import java.io.File;
034import java.nio.file.FileSystems;
035import java.nio.file.Path;
036import java.util.HashMap;
037import static io.stallion.utils.Literals.*;
038import static io.stallion.Context.*;
039
040/**
041 * Manages assets that can be included on web pages. Takes care of cache-busting URL's
042 * and asset bundling
043 */
044public class AssetsController {
045
046    private static AssetsController _instance;
047
048    public static AssetsController instance() {
049        if (_instance == null) {
050            throw new UsageException("Must call load() before accessing the AssetsController instance");
051        }
052        return _instance;
053    }
054
055    public static String ensureSafeAssetsPath(String path) {
056        if (!path.startsWith("/")) {
057            path = "/" + path;
058        }
059        if (!path.startsWith("/assets/")) {
060            path = "/assets" + path;
061        }
062        if (path.contains("..")) {
063            throw new UsageException("Invalid asset path, illegal characters: " + path);
064        }
065        return path;
066    }
067
068    /**
069     * Get a wrapper for the asset controller with limited methods. This is used by
070     * the template context and other sandboxed situations.
071     *
072     * @return
073     */
074    public static AssetsControllerSafeWrapper wrapper() {
075        return instance().getWrapper();
076    }
077
078
079
080    public static AssetsController load() {
081        _instance = new AssetsController();
082        if (new File(Settings.instance().getTargetFolder() + "/assets").isDirectory()) {
083            /*
084            FileSystemWatcherService.instance().registerWatcher(
085                    new AssetFileChangeEventHandler()
086                            .setWatchedFolder(Settings.instance().getTargetFolder() + "/assets")
087                            .setWatchTree(true)
088            );*/
089        }
090        // Load the pre-processors;
091        //ExternalCommandPreProcessorRegistry.instance();
092        return _instance;
093    }
094
095    public static void shutdown() {
096        _instance = null;
097    }
098
099    private AssetsControllerSafeWrapper wrapper;
100
101
102    private static HashMap<String, Long> timeStampByPath = new HashMap<String, Long>();
103
104    public static HashMap<String, Long> getTimeStampByPath() {
105        return timeStampByPath;
106    }
107
108    public static void setTimeStampByPath(HashMap<String, Long> timeStampByPath) {
109        AssetsController.timeStampByPath = timeStampByPath;
110    }
111
112    /**
113     * Loads a resource from the main stallion jar.
114     *
115     * @param path
116     * @return
117     */
118    public String resource(String path) {
119        return resource(path, "stallion");
120    }
121
122    public String resource(String path, String plugin) {
123        return resource(path, plugin, "");
124    }
125
126    /**
127     * Get the URL to access an asset file that is bundled in the jar as a resource.
128     *
129     * @param path - the path, relative to the assets folder in side the resource directory
130     * @param plugin - the plugin from which you are loading the resource, use "stallion" to load from the main jar
131     * @param developerUrl - an alternative URL to use in development mode. Useful if you want to point
132     *                     you local nginx directly at the file, so you can see your changes without recompiling.
133     * @return
134     */
135    public String resource(String path, String plugin, String developerUrl) {
136        if (Context.getSettings().getDevMode() && !empty(developerUrl)) {
137            return developerUrl;
138        }
139
140        if (path.startsWith("/")) {
141            path = path.substring(1);
142        }
143        return Context.settings().getCdnUrl() + "/st-resource/" + plugin + "/" + path;
144    }
145
146    /**
147     * Turn a list of additional strings that should be in the Footer section of the
148     * page and return as a string
149     *
150     * @return
151     */
152    public String pageFooterLiterals() {
153        return Context.getResponse().getPageFooterLiterals().stringify();
154    }
155
156    /**
157     * Turn a list of additional strings that should be in the HEAD section of the
158     * page and return as a string
159     *
160     * @return
161     */
162    public String pageHeadLiterals() {
163        return Context.getResponse().getPageHeadLiterals().stringify();
164    }
165
166
167    public String bundle(String plugin, String path) {
168        if (Settings.instance().getBundleDebug()) {
169            return new ResourceAssetBundleRenderer(plugin, path).renderDebugHtml();
170        } else {
171            return new ResourceAssetBundleRenderer(plugin, path).renderProductionHtml();
172        }
173    }
174
175
176    /**
177     * Output the HTML required to render a bundle of assets.
178     * *
179     * @param fileName
180     * @return
181     */
182    public String bundle(String fileName) {
183        if (Settings.instance().getBundleDebug()) {
184            return new FileSystemAssetBundleRenderer(fileName).renderDebugHtml();
185        } else {
186            return new FileSystemAssetBundleRenderer(fileName).renderProductionHtml();
187        }
188    }
189
190
191    /**
192     * Get the URL for an asset file, with a timestamp added for cache busting.
193     * @param path
194     * @return
195     */
196    public String url(String path) {
197        if (path.startsWith("/")) {
198            path = path.substring(1);
199        }
200        String url = Context.settings().getCdnUrl() + "/st-assets/" + path;
201        if (url.contains("?")) {
202            url = url + "&";
203        } else {
204            url = url + "?";
205        }
206        url = url + "ts=" +  getTimeStampForAssetFile(path).toString();
207        return url;
208    }
209
210
211
212    public Long getTimeStampForAssetFile(String path) {
213        String filePath = Context.settings().getTargetFolder() + "/assets/" + path;
214        if (getTimeStampByPath().containsKey(filePath)) {
215            Long ts = getTimeStampByPath().get(filePath);
216            if (ts > 0) {
217                return ts;
218            }
219        }
220
221        Path pathObj = FileSystems.getDefault().getPath(filePath);
222        File file = new File(filePath);
223        Long ts = file.lastModified();
224        getTimeStampByPath().put(filePath, ts);
225        return ts;
226    }
227
228    public Long getCurrentTimeStampForAssetFile(String path) {
229        String filePath = Context.settings().getTargetFolder() + "/assets/" + path;
230        Path pathObj = FileSystems.getDefault().getPath(filePath);
231        File file = new File(filePath);
232        Long ts = file.lastModified();
233        return ts;
234    }
235
236
237    private String getKeyStringForPathSource(String path, String source) {
238        Long ts = getCurrentTimeStampForAssetFile(path);
239        if (ts == 0 || ts == null) {
240            return DigestUtils.md5Hex(source);
241        } else {
242            return ts.toString();
243        }
244    }
245
246
247    public AssetsControllerSafeWrapper getWrapper() {
248        if (wrapper == null) {
249            wrapper = new AssetsControllerSafeWrapper(this);
250        }
251        return wrapper;
252    }
253
254}