001
014
015 package com.liferay.portal.servlet.filters.dynamiccss;
016
017 import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
018 import com.liferay.portal.kernel.io.unsync.UnsyncPrintWriter;
019 import com.liferay.portal.kernel.log.Log;
020 import com.liferay.portal.kernel.log.LogFactoryUtil;
021 import com.liferay.portal.kernel.util.CharPool;
022 import com.liferay.portal.kernel.util.JavaConstants;
023 import com.liferay.portal.kernel.util.ParamUtil;
024 import com.liferay.portal.kernel.util.SessionParamUtil;
025 import com.liferay.portal.kernel.util.StringBundler;
026 import com.liferay.portal.kernel.util.StringPool;
027 import com.liferay.portal.kernel.util.StringUtil;
028 import com.liferay.portal.kernel.util.UnsyncPrintWriterPool;
029 import com.liferay.portal.kernel.util.Validator;
030 import com.liferay.portal.kernel.util.WebKeys;
031 import com.liferay.portal.model.PortletConstants;
032 import com.liferay.portal.model.Theme;
033 import com.liferay.portal.scripting.ruby.RubyExecutor;
034 import com.liferay.portal.service.ThemeLocalServiceUtil;
035 import com.liferay.portal.theme.ThemeDisplay;
036 import com.liferay.portal.tools.SassToCssBuilder;
037 import com.liferay.portal.util.ClassLoaderUtil;
038 import com.liferay.portal.util.PortalUtil;
039 import com.liferay.portal.util.PropsValues;
040
041 import java.io.File;
042
043 import java.net.URL;
044 import java.net.URLConnection;
045 import java.net.URLDecoder;
046
047 import java.util.HashMap;
048 import java.util.Map;
049 import java.util.regex.Matcher;
050 import java.util.regex.Pattern;
051
052 import javax.servlet.ServletContext;
053 import javax.servlet.http.HttpServletRequest;
054
055 import org.apache.commons.lang.time.StopWatch;
056
057
061 public class DynamicCSSUtil {
062
063 public static void init() {
064 try {
065 _rubyScript = StringUtil.read(
066 ClassLoaderUtil.getPortalClassLoader(),
067 "com/liferay/portal/servlet/filters/dynamiccss/main.rb");
068 }
069 catch (Exception e) {
070 _log.error(e, e);
071 }
072 }
073
074 public static String parseSass(
075 ServletContext servletContext, HttpServletRequest request,
076 String resourcePath, String content)
077 throws Exception {
078
079 if (!DynamicCSSFilter.ENABLED) {
080 return content;
081 }
082
083 StopWatch stopWatch = null;
084
085 if (_log.isDebugEnabled()) {
086 stopWatch = new StopWatch();
087
088 stopWatch.start();
089 }
090
091
092
093 if (request == null) {
094 return content;
095 }
096
097 ThemeDisplay themeDisplay = (ThemeDisplay)request.getAttribute(
098 WebKeys.THEME_DISPLAY);
099
100 Theme theme = null;
101
102 if (themeDisplay == null) {
103 theme = _getTheme(request);
104
105 if (theme == null) {
106 String currentURL = PortalUtil.getCurrentURL(request);
107
108 if (_log.isWarnEnabled()) {
109 _log.warn("No theme found for " + currentURL);
110 }
111
112 return content;
113 }
114 }
115
116 String parsedContent = null;
117
118 boolean themeCssFastLoad = _isThemeCssFastLoad(request, themeDisplay);
119
120 URLConnection resourceURLConnection = null;
121
122 URL resourceURL = servletContext.getResource(resourcePath);
123
124 if (resourceURL != null) {
125 resourceURLConnection = resourceURL.openConnection();
126 }
127
128 URLConnection cacheResourceURLConnection = null;
129
130 URL cacheResourceURL = _getCacheResource(servletContext, resourcePath);
131
132 if (cacheResourceURL != null) {
133 cacheResourceURLConnection = cacheResourceURL.openConnection();
134 }
135
136 if (themeCssFastLoad && (cacheResourceURLConnection != null) &&
137 (resourceURLConnection != null) &&
138 (cacheResourceURLConnection.getLastModified() ==
139 resourceURLConnection.getLastModified())) {
140
141 parsedContent = StringUtil.read(
142 cacheResourceURLConnection.getInputStream());
143
144 if (_log.isDebugEnabled()) {
145 _log.debug(
146 "Loading SASS cache from " + cacheResourceURL.getPath() +
147 " takes " + stopWatch.getTime() + " ms");
148 }
149 }
150 else {
151 content = SassToCssBuilder.parseStaticTokens(content);
152
153 String queryString = request.getQueryString();
154
155 if (!themeCssFastLoad && Validator.isNotNull(queryString)) {
156 content = propagateQueryString(content, queryString);
157 }
158
159 parsedContent = _parseSass(
160 servletContext, request, themeDisplay, theme, resourcePath,
161 content);
162
163 if (_log.isDebugEnabled()) {
164 _log.debug(
165 "Parsing SASS for " + resourcePath + " takes " +
166 stopWatch.getTime() + " ms");
167 }
168 }
169
170 if (Validator.isNull(parsedContent)) {
171 return content;
172 }
173
174 parsedContent = StringUtil.replace(
175 parsedContent,
176 new String[] {
177 "@portal_ctx@", "@theme_image_path@"
178 },
179 new String[] {
180 PortalUtil.getPathContext(),
181 _getThemeImagesPath(request, themeDisplay, theme)
182 });
183
184 return parsedContent;
185 }
186
187 private static URL _getCacheResource(
188 ServletContext servletContext, String resourcePath) throws Exception {
189
190 int pos = resourcePath.lastIndexOf(StringPool.SLASH);
191
192 String cacheFileName =
193 resourcePath.substring(0, pos + 1) + ".sass-cache/" +
194 resourcePath.substring(pos + 1);
195
196 return servletContext.getResource(cacheFileName);
197 }
198
199 private static String _getCssThemePath(
200 HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme)
201 throws Exception {
202
203 String cssThemePath = null;
204
205 if (themeDisplay != null) {
206 cssThemePath = themeDisplay.getPathThemeCss();
207 }
208 else {
209 String cdnHost = StringPool.BLANK;
210
211 if (PortalUtil.isCDNDynamicResourcesEnabled(request)) {
212 cdnHost = PortalUtil.getCDNHost(request);
213 }
214
215 String themeStaticResourcePath = theme.getStaticResourcePath();
216
217 cssThemePath =
218 cdnHost + themeStaticResourcePath + theme.getCssPath();
219 }
220
221 return cssThemePath;
222 }
223
224 private static File _getSassTempDir(ServletContext servletContext) {
225 File sassTempDir = (File)servletContext.getAttribute(_SASS_DIR_KEY);
226
227 if (sassTempDir != null) {
228 return sassTempDir;
229 }
230
231 File tempDir = (File)servletContext.getAttribute(
232 JavaConstants.JAVAX_SERVLET_CONTEXT_TEMPDIR);
233
234 sassTempDir = new File(tempDir, _SASS_DIR);
235
236 sassTempDir.mkdirs();
237
238 servletContext.setAttribute(_SASS_DIR_KEY, sassTempDir);
239
240 return sassTempDir;
241 }
242
243 private static Theme _getTheme(HttpServletRequest request)
244 throws Exception {
245
246 long companyId = PortalUtil.getCompanyId(request);
247
248 String themeId = ParamUtil.getString(request, "themeId");
249
250 if (Validator.isNotNull(themeId)) {
251 try {
252 Theme theme = ThemeLocalServiceUtil.getTheme(
253 companyId, themeId, false);
254
255 return theme;
256 }
257 catch (Exception e) {
258 _log.error(e, e);
259 }
260 }
261
262 String requestURI = URLDecoder.decode(
263 request.getRequestURI(), StringPool.UTF8);
264
265 Matcher portalThemeMatcher = _portalThemePattern.matcher(requestURI);
266
267 if (portalThemeMatcher.find()) {
268 String themePathId = portalThemeMatcher.group(1);
269
270 themePathId = StringUtil.replace(
271 themePathId, StringPool.UNDERLINE, StringPool.BLANK);
272
273 themeId = PortalUtil.getJsSafePortletId(themePathId);
274 }
275 else {
276 Matcher pluginThemeMatcher = _pluginThemePattern.matcher(
277 requestURI);
278
279 if (pluginThemeMatcher.find()) {
280 String themePathId = pluginThemeMatcher.group(1);
281
282 themePathId = StringUtil.replace(
283 themePathId, StringPool.UNDERLINE, StringPool.BLANK);
284
285 StringBundler sb = new StringBundler(4);
286
287 sb.append(themePathId);
288 sb.append(PortletConstants.WAR_SEPARATOR);
289 sb.append(themePathId);
290 sb.append("theme");
291
292 themePathId = sb.toString();
293
294 themeId = PortalUtil.getJsSafePortletId(themePathId);
295 }
296 }
297
298 if (Validator.isNull(themeId)) {
299 return null;
300 }
301
302 try {
303 Theme theme = ThemeLocalServiceUtil.getTheme(
304 companyId, themeId, false);
305
306 return theme;
307 }
308 catch (Exception e) {
309 _log.error(e, e);
310 }
311
312 return null;
313 }
314
315 private static String _getThemeImagesPath(
316 HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme)
317 throws Exception {
318
319 String themeImagesPath = null;
320
321 if (themeDisplay != null) {
322 themeImagesPath = themeDisplay.getPathThemeImages();
323 }
324 else {
325 String cdnHost = PortalUtil.getCDNHost(request);
326 String themeStaticResourcePath = theme.getStaticResourcePath();
327
328 themeImagesPath =
329 cdnHost + themeStaticResourcePath + theme.getImagesPath();
330 }
331
332 return themeImagesPath;
333 }
334
335 private static boolean _isThemeCssFastLoad(
336 HttpServletRequest request, ThemeDisplay themeDisplay) {
337
338 if (themeDisplay != null) {
339 return themeDisplay.isThemeCssFastLoad();
340 }
341
342 return SessionParamUtil.getBoolean(
343 request, "css_fast_load", PropsValues.THEME_CSS_FAST_LOAD);
344 }
345
346 private static String _parseSass(
347 ServletContext servletContext, HttpServletRequest request,
348 ThemeDisplay themeDisplay, Theme theme, String resourcePath,
349 String content)
350 throws Exception {
351
352 Map<String, Object> inputObjects = new HashMap<String, Object>();
353
354 inputObjects.put("content", content);
355 inputObjects.put("cssRealPath", resourcePath);
356 inputObjects.put(
357 "cssThemePath", _getCssThemePath(request, themeDisplay, theme));
358
359 File sassTempDir = _getSassTempDir(servletContext);
360
361 inputObjects.put("sassCachePath", sassTempDir.getCanonicalPath());
362
363 UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
364 new UnsyncByteArrayOutputStream();
365
366 UnsyncPrintWriter unsyncPrintWriter = UnsyncPrintWriterPool.borrow(
367 unsyncByteArrayOutputStream);
368
369 inputObjects.put("out", unsyncPrintWriter);
370
371 _rubyExecutor.eval(null, inputObjects, null, _rubyScript);
372
373 unsyncPrintWriter.flush();
374
375 return unsyncByteArrayOutputStream.toString();
376 }
377
378
381 private static String propagateQueryString(
382 String content, String queryString) {
383
384 StringBuilder sb = new StringBuilder(content.length());
385
386 int pos = 0;
387
388 while (true) {
389 int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
390 int importY = content.indexOf(
391 _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
392
393 if ((importX == -1) || (importY == -1)) {
394 sb.append(content.substring(pos));
395
396 break;
397 }
398
399 sb.append(content.substring(pos, importY));
400 sb.append(CharPool.QUESTION);
401 sb.append(queryString);
402 sb.append(_CSS_IMPORT_END);
403
404 pos = importY + _CSS_IMPORT_END.length();
405 }
406
407 return sb.toString();
408 }
409
410 private static final String _CSS_IMPORT_BEGIN = "@import url(";
411
412 private static final String _CSS_IMPORT_END = ");";
413
414 private static final String _SASS_DIR = "sass";
415
416 private static final String _SASS_DIR_KEY =
417 DynamicCSSUtil.class.getName() + "#sass";
418
419 private static Log _log = LogFactoryUtil.getLog(DynamicCSSUtil.class);
420
421 private static Pattern _pluginThemePattern = Pattern.compile(
422 "\\/([^\\/]+)-theme\\/", Pattern.CASE_INSENSITIVE);
423 private static Pattern _portalThemePattern = Pattern.compile(
424 "themes\\/([^\\/]+)\\/css", Pattern.CASE_INSENSITIVE);
425 private static RubyExecutor _rubyExecutor = new RubyExecutor();
426 private static String _rubyScript;
427
428 }