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