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