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}