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