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