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