001
014
015 package com.liferay.portal.servlet.filters.dynamiccss;
016
017 import com.liferay.portal.kernel.log.Log;
018 import com.liferay.portal.kernel.log.LogFactoryUtil;
019 import com.liferay.portal.kernel.util.CharPool;
020 import com.liferay.portal.kernel.util.FileUtil;
021 import com.liferay.portal.kernel.util.JavaConstants;
022 import com.liferay.portal.kernel.util.ParamUtil;
023 import com.liferay.portal.kernel.util.ServerDetector;
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.URLUtil;
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.service.ThemeLocalServiceUtil;
034 import com.liferay.portal.theme.ThemeDisplay;
035 import com.liferay.portal.tools.CSSBuilderUtil;
036 import com.liferay.portal.util.PortalUtil;
037 import com.liferay.portal.util.PropsValues;
038 import com.liferay.sass.compiler.SassCompiler;
039 import com.liferay.sass.compiler.jni.internal.JniSassCompiler;
040 import com.liferay.sass.compiler.ruby.internal.RubySassCompiler;
041
042 import java.io.File;
043
044 import java.net.URL;
045 import java.net.URLConnection;
046 import java.net.URLDecoder;
047
048 import java.util.regex.Matcher;
049 import java.util.regex.Pattern;
050
051 import javax.servlet.ServletContext;
052 import javax.servlet.http.HttpServletRequest;
053
054 import org.apache.commons.lang.time.StopWatch;
055
056
061 public class DynamicCSSUtil {
062
063 public static void init(ServletContext servletContext) {
064 try {
065 if (_initialized) {
066 return;
067 }
068
069 try {
070 _sassCompiler = new JniSassCompiler();
071 }
072 catch (Throwable t) {
073 File sassTempDir = _getSassTempDir(servletContext);
074
075 _sassCompiler = new RubySassCompiler(
076 PropsValues.SCRIPTING_JRUBY_COMPILE_MODE,
077 PropsValues.SCRIPTING_JRUBY_COMPILE_THRESHOLD,
078 sassTempDir.getCanonicalPath());
079 }
080
081 _initialized = true;
082 }
083 catch (Exception e) {
084 _log.error(e, e);
085 }
086 }
087
088 public static String parseSass(
089 ServletContext servletContext, HttpServletRequest request,
090 String resourcePath, String content)
091 throws Exception {
092
093 if (!DynamicCSSFilter.ENABLED) {
094 return content;
095 }
096
097 StopWatch stopWatch = new StopWatch();
098
099 stopWatch.start();
100
101
102
103 if (request == null) {
104 return content;
105 }
106
107 ThemeDisplay themeDisplay = (ThemeDisplay)request.getAttribute(
108 WebKeys.THEME_DISPLAY);
109
110 Theme theme = null;
111
112 if (themeDisplay == null) {
113 theme = _getTheme(request);
114
115 if (theme == null) {
116 String currentURL = PortalUtil.getCurrentURL(request);
117
118 if (_log.isWarnEnabled()) {
119 _log.warn("No theme found for " + currentURL);
120 }
121
122 if (PortalUtil.isRightToLeft(request) &&
123 !RTLCSSUtil.isExcludedPath(resourcePath)) {
124
125 content = RTLCSSUtil.getRtlCss(resourcePath, content);
126 }
127
128 return content;
129 }
130 }
131
132 String parsedContent = null;
133
134 boolean themeCssFastLoad = _isThemeCssFastLoad(request, themeDisplay);
135
136 URL cacheResourceURL = _getCacheResourceURL(
137 servletContext, request, resourcePath);
138
139 if (cacheResourceURL != null) {
140 if (!themeCssFastLoad) {
141 URL resourceURL = servletContext.getResource(resourcePath);
142
143 if (resourceURL != null) {
144 if (URLUtil.getLastModifiedTime(cacheResourceURL) <
145 URLUtil.getLastModifiedTime(resourceURL)) {
146
147 cacheResourceURL = null;
148 }
149 }
150 }
151 }
152
153 if ((themeCssFastLoad || !content.contains(_CSS_IMPORT_BEGIN)) &&
154 (cacheResourceURL != null)) {
155
156 parsedContent = StringUtil.read(cacheResourceURL.openStream());
157
158 if (_log.isDebugEnabled()) {
159 _log.debug(
160 "Loading SASS cache from " + cacheResourceURL.getPath() +
161 " takes " + stopWatch.getTime() + " ms");
162 }
163 }
164 else {
165 content = CSSBuilderUtil.parseStaticTokens(content);
166
167 String queryString = request.getQueryString();
168
169 if (!themeCssFastLoad && Validator.isNotNull(queryString)) {
170 content = propagateQueryString(content, queryString);
171 }
172
173 if (!themeCssFastLoad && _isImportsOnly(content)) {
174 parsedContent = content;
175 }
176 else {
177 parsedContent = _parseSass(
178 servletContext, request, themeDisplay, theme, content);
179 }
180
181 if (PortalUtil.isRightToLeft(request) &&
182 !RTLCSSUtil.isExcludedPath(resourcePath)) {
183
184 parsedContent = RTLCSSUtil.getRtlCss(
185 resourcePath, parsedContent);
186
187
188
189 URL rtlCustomResourceURL = _getRtlCustomResourceURL(
190 servletContext, resourcePath);
191
192 if (rtlCustomResourceURL != null) {
193 URLConnection rtlCustomResourceURLConnection =
194 rtlCustomResourceURL.openConnection();
195
196 String rtlCustomContent = StringUtil.read(
197 rtlCustomResourceURLConnection.getInputStream());
198
199 String parsedRtlCustomContent = _parseSass(
200 servletContext, request, themeDisplay, theme,
201 rtlCustomContent);
202
203 parsedContent += parsedRtlCustomContent;
204 }
205 }
206
207 if (_log.isDebugEnabled()) {
208 _log.debug(
209 "Parsing SASS for " + resourcePath + " takes " +
210 stopWatch.getTime() + " ms");
211 }
212 }
213
214 if (Validator.isNull(parsedContent)) {
215 return content;
216 }
217
218 parsedContent = replaceToken(
219 servletContext, request, themeDisplay, theme, parsedContent);
220
221 return parsedContent;
222 }
223
224 public static String replaceToken(
225 ServletContext servletContext, HttpServletRequest request,
226 String content)
227 throws Exception {
228
229 ThemeDisplay themeDisplay = (ThemeDisplay)request.getAttribute(
230 WebKeys.THEME_DISPLAY);
231
232 Theme theme = _getTheme(request);
233
234 if (theme == null) {
235 return content;
236 }
237
238 return replaceToken(
239 servletContext, request, themeDisplay, theme, content);
240 }
241
242 public static String replaceToken(
243 ServletContext servletContext, HttpServletRequest request,
244 ThemeDisplay themeDisplay, Theme theme, String parsedContent)
245 throws Exception {
246
247 String portalContextPath = PortalUtil.getPathContext();
248
249 String baseURL = servletContext.getContextPath();
250
251 if (baseURL.endsWith(StringPool.SLASH)) {
252 baseURL = baseURL.substring(0, baseURL.length() - 1);
253 }
254
255 parsedContent = StringUtil.replace(
256 parsedContent,
257 new String[] {"@base_url@", "@portal_ctx@", "@theme_image_path@"},
258 new String[] {
259 baseURL, portalContextPath,
260 _getThemeImagesPath(request, themeDisplay, theme)
261 });
262
263 return parsedContent;
264 }
265
266
270 protected static String propagateQueryString(
271 String content, String queryString) {
272
273 StringBuilder sb = new StringBuilder(content.length());
274
275 int pos = 0;
276
277 while (true) {
278 int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
279 int importY = content.indexOf(
280 _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
281
282 if ((importX == -1) || (importY == -1)) {
283 sb.append(content.substring(pos));
284
285 break;
286 }
287
288 sb.append(content.substring(pos, importX));
289 sb.append(_CSS_IMPORT_BEGIN);
290
291 String url = content.substring(
292 importX + _CSS_IMPORT_BEGIN.length(), importY);
293
294 char firstChar = url.charAt(0);
295
296 if (firstChar == CharPool.APOSTROPHE) {
297 sb.append(CharPool.APOSTROPHE);
298 }
299 else if (firstChar == CharPool.QUOTE) {
300 sb.append(CharPool.QUOTE);
301 }
302
303 url = StringUtil.unquote(url);
304
305 sb.append(url);
306
307 if (url.indexOf(CharPool.QUESTION) != -1) {
308 sb.append(CharPool.AMPERSAND);
309 }
310 else {
311 sb.append(CharPool.QUESTION);
312 }
313
314 sb.append(queryString);
315
316 if (firstChar == CharPool.APOSTROPHE) {
317 sb.append(CharPool.APOSTROPHE);
318 }
319 else if (firstChar == CharPool.QUOTE) {
320 sb.append(CharPool.QUOTE);
321 }
322
323 sb.append(_CSS_IMPORT_END);
324
325 pos = importY + _CSS_IMPORT_END.length();
326 }
327
328 return sb.toString();
329 }
330
331 private static URL _getCacheResourceURL(
332 ServletContext servletContext, HttpServletRequest request,
333 String resourcePath)
334 throws Exception {
335
336 String suffix = StringPool.BLANK;
337
338 if (PortalUtil.isRightToLeft(request) &&
339 !RTLCSSUtil.isExcludedPath(resourcePath)) {
340
341 suffix = "_rtl";
342 }
343
344 return servletContext.getResource(
345 CSSBuilderUtil.getCacheFileName(resourcePath, suffix));
346 }
347
348 private static String _getCssThemePath(
349 ServletContext servletContext, HttpServletRequest request,
350 ThemeDisplay themeDisplay, Theme theme)
351 throws Exception {
352
353 if (themeDisplay != null) {
354 return themeDisplay.getPathThemeCss();
355 }
356
357 if (PortalUtil.isCDNDynamicResourcesEnabled(request)) {
358 String cdnHost = PortalUtil.getCDNHost(request);
359
360 if (Validator.isNotNull(cdnHost)) {
361 return cdnHost.concat(theme.getStaticResourcePath()).concat(
362 theme.getCssPath());
363 }
364 }
365
366 return servletContext.getRealPath(theme.getCssPath());
367 }
368
369 private static URL _getRtlCustomResourceURL(
370 ServletContext servletContext, String resourcePath)
371 throws Exception {
372
373 return servletContext.getResource(
374 CSSBuilderUtil.getRtlCustomFileName(resourcePath));
375 }
376
377 private static File _getSassTempDir(ServletContext servletContext) {
378 File sassTempDir = (File)servletContext.getAttribute(_SASS_DIR_KEY);
379
380 if (sassTempDir != null) {
381 return sassTempDir;
382 }
383
384 File tempDir = (File)servletContext.getAttribute(
385 JavaConstants.JAVAX_SERVLET_CONTEXT_TEMPDIR);
386
387 sassTempDir = new File(tempDir, _SASS_DIR);
388
389 sassTempDir.mkdirs();
390
391 servletContext.setAttribute(_SASS_DIR_KEY, sassTempDir);
392
393 return sassTempDir;
394 }
395
396 private static Theme _getTheme(HttpServletRequest request)
397 throws Exception {
398
399 long companyId = PortalUtil.getCompanyId(request);
400
401 String themeId = ParamUtil.getString(request, "themeId");
402
403 if (Validator.isNotNull(themeId)) {
404 try {
405 Theme theme = ThemeLocalServiceUtil.getTheme(
406 companyId, themeId, false);
407
408 return theme;
409 }
410 catch (Exception e) {
411 _log.error(e, e);
412 }
413 }
414
415 String requestURI = URLDecoder.decode(
416 request.getRequestURI(), StringPool.UTF8);
417
418 Matcher portalThemeMatcher = _portalThemePattern.matcher(requestURI);
419
420 if (portalThemeMatcher.find()) {
421 String themePathId = portalThemeMatcher.group(1);
422
423 themePathId = StringUtil.replace(
424 themePathId, StringPool.UNDERLINE, StringPool.BLANK);
425
426 themeId = PortalUtil.getJsSafePortletId(themePathId);
427 }
428 else {
429 Matcher pluginThemeMatcher = _pluginThemePattern.matcher(
430 requestURI);
431
432 if (pluginThemeMatcher.find()) {
433 String themePathId = pluginThemeMatcher.group(1);
434
435 themePathId = StringUtil.replace(
436 themePathId, StringPool.UNDERLINE, StringPool.BLANK);
437
438 StringBundler sb = new StringBundler(4);
439
440 sb.append(themePathId);
441 sb.append(PortletConstants.WAR_SEPARATOR);
442 sb.append(themePathId);
443 sb.append("theme");
444
445 themePathId = sb.toString();
446
447 themeId = PortalUtil.getJsSafePortletId(themePathId);
448 }
449 }
450
451 if (Validator.isNull(themeId)) {
452 return null;
453 }
454
455 try {
456 Theme theme = ThemeLocalServiceUtil.getTheme(
457 companyId, themeId, false);
458
459 return theme;
460 }
461 catch (Exception e) {
462 _log.error(e, e);
463 }
464
465 return null;
466 }
467
468 private static String _getThemeImagesPath(
469 HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme)
470 throws Exception {
471
472 String themeImagesPath = null;
473
474 if (themeDisplay != null) {
475 themeImagesPath = themeDisplay.getPathThemeImages();
476 }
477 else {
478 String cdnHost = PortalUtil.getCDNHost(request);
479 String themeStaticResourcePath = theme.getStaticResourcePath();
480
481 themeImagesPath =
482 cdnHost + themeStaticResourcePath + theme.getImagesPath();
483 }
484
485 return themeImagesPath;
486 }
487
488 private static boolean _isImportsOnly(String content) {
489 int pos = 0;
490
491 while (true) {
492 int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
493 int importY = content.indexOf(
494 _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
495
496 if ((importX == -1) || (importY == -1)) {
497 String substring = content.substring(pos);
498
499 substring = substring.trim();
500
501 if (substring.isEmpty()) {
502 return true;
503 }
504 else {
505 return false;
506 }
507 }
508
509 String substring = content.substring(pos, importX);
510
511 substring = substring.trim();
512
513 if (!substring.isEmpty()) {
514 return false;
515 }
516
517 pos = importY + _CSS_IMPORT_END.length();
518 }
519 }
520
521 private static boolean _isThemeCssFastLoad(
522 HttpServletRequest request, ThemeDisplay themeDisplay) {
523
524 if (!PropsValues.THEME_CSS_FAST_LOAD_CHECK_REQUEST_PARAMETER) {
525 return PropsValues.THEME_CSS_FAST_LOAD;
526 }
527
528 if (themeDisplay != null) {
529 return themeDisplay.isThemeCssFastLoad();
530 }
531
532 return SessionParamUtil.getBoolean(
533 request, "css_fast_load", PropsValues.THEME_CSS_FAST_LOAD);
534 }
535
536 private static String _parseSass(
537 ServletContext servletContext, HttpServletRequest request,
538 ThemeDisplay themeDisplay, Theme theme, String content)
539 throws Exception {
540
541 String portalWebDir = PortalUtil.getPortalWebDir();
542
543 String commonSassPath = portalWebDir.concat(_SASS_COMMON_DIR);
544 String cssThemePath = _getCssThemePath(
545 servletContext, request, themeDisplay, theme);
546
547 if (ServerDetector.isWebLogic() && !FileUtil.exists(commonSassPath)) {
548 int pos = cssThemePath.indexOf("autodeploy/");
549
550 if (pos == -1) {
551 if (_log.isWarnEnabled()) {
552 _log.warn("Dynamic CSS compilation may not work");
553 }
554 }
555 else {
556 commonSassPath =
557 cssThemePath.substring(0, pos + 11) + "ROOT/" +
558 _SASS_COMMON_DIR;
559 }
560 }
561
562 try {
563 content = _sassCompiler.compileString(
564 content, commonSassPath + File.pathSeparator + cssThemePath,
565 "");
566 }
567 catch (Exception e) {
568 _log.error(e, e);
569 }
570
571 return content;
572 }
573
574 private static final String _CSS_IMPORT_BEGIN = "@import url(";
575
576 private static final String _CSS_IMPORT_END = ");";
577
578 private static final String _SASS_COMMON_DIR = "/html/css/common";
579
580 private static final String _SASS_DIR = "sass";
581
582 private static final String _SASS_DIR_KEY =
583 DynamicCSSUtil.class.getName() + "#sass";
584
585 private static final Log _log = LogFactoryUtil.getLog(DynamicCSSUtil.class);
586
587 private static boolean _initialized;
588 private static final Pattern _pluginThemePattern = Pattern.compile(
589 "\\/([^\\/]+)-theme\\/", Pattern.CASE_INSENSITIVE);
590 private static final Pattern _portalThemePattern = Pattern.compile(
591 "themes\\/([^\\/]+)\\/css", Pattern.CASE_INSENSITIVE);
592 private static SassCompiler _sassCompiler;
593
594 }