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.security.pacl.PACLClassLoaderUtil;
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.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 PACLClassLoaderUtil.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) throws Exception {
189
190 int pos = resourcePath.lastIndexOf(StringPool.SLASH);
191
192 String cacheFileName =
193 resourcePath.substring(0, pos + 1) + ".sass-cache/" +
194 resourcePath.substring(pos + 1);
195
196 return servletContext.getResource(cacheFileName);
197 }
198
199 private static String _getCssThemePath(
200 HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme)
201 throws Exception {
202
203 String cssThemePath = null;
204
205 if (themeDisplay != null) {
206 cssThemePath = themeDisplay.getPathThemeCss();
207 }
208 else {
209 String cdnHost = StringPool.BLANK;
210
211 if (PortalUtil.isCDNDynamicResourcesEnabled(request)) {
212 cdnHost = PortalUtil.getCDNHost(request);
213 }
214
215 String themeStaticResourcePath = theme.getStaticResourcePath();
216
217 cssThemePath =
218 cdnHost + themeStaticResourcePath + theme.getCssPath();
219 }
220
221 return cssThemePath;
222 }
223
224 private static File _getSassTempDir(ServletContext servletContext) {
225 File sassTempDir = (File)servletContext.getAttribute(_SASS_DIR_KEY);
226
227 if (sassTempDir != null) {
228 return sassTempDir;
229 }
230
231 File tempDir = (File)servletContext.getAttribute(
232 JavaConstants.JAVAX_SERVLET_CONTEXT_TEMPDIR);
233
234 sassTempDir = new File(tempDir, _SASS_DIR);
235
236 sassTempDir.mkdirs();
237
238 servletContext.setAttribute(_SASS_DIR_KEY, sassTempDir);
239
240 return sassTempDir;
241 }
242
243 private static Theme _getTheme(HttpServletRequest request)
244 throws Exception {
245
246 long companyId = PortalUtil.getCompanyId(request);
247
248 String themeId = ParamUtil.getString(request, "themeId");
249
250 String requestURI = URLDecoder.decode(
251 request.getRequestURI(), StringPool.UTF8);
252
253 Matcher portalThemeMatcher = _portalThemePattern.matcher(requestURI);
254
255 if (portalThemeMatcher.find()) {
256 String themePathId = portalThemeMatcher.group(1);
257
258 themePathId = StringUtil.replace(
259 themePathId, StringPool.UNDERLINE, StringPool.BLANK);
260
261 themeId = PortalUtil.getJsSafePortletId(themePathId);
262 }
263 else {
264 Matcher pluginThemeMatcher = _pluginThemePattern.matcher(
265 requestURI);
266
267 if (pluginThemeMatcher.find()) {
268 String themePathId = pluginThemeMatcher.group(1);
269
270 themePathId = StringUtil.replace(
271 themePathId, StringPool.UNDERLINE, StringPool.BLANK);
272
273 StringBundler sb = new StringBundler(4);
274
275 sb.append(themePathId);
276 sb.append(PortletConstants.WAR_SEPARATOR);
277 sb.append(themePathId);
278 sb.append("theme");
279
280 themePathId = sb.toString();
281
282 themeId = PortalUtil.getJsSafePortletId(themePathId);
283 }
284 }
285
286 if (Validator.isNull(themeId)) {
287 return null;
288 }
289
290 try {
291 Theme theme = ThemeLocalServiceUtil.getTheme(
292 companyId, themeId, false);
293
294 return theme;
295 }
296 catch (Exception e) {
297 _log.error(e, e);
298 }
299
300 return null;
301 }
302
303 private static String _getThemeImagesPath(
304 HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme)
305 throws Exception {
306
307 String themeImagesPath = null;
308
309 if (themeDisplay != null) {
310 themeImagesPath = themeDisplay.getPathThemeImages();
311 }
312 else {
313 String cdnHost = PortalUtil.getCDNHost(request);
314 String themeStaticResourcePath = theme.getStaticResourcePath();
315
316 themeImagesPath =
317 cdnHost + themeStaticResourcePath + theme.getImagesPath();
318 }
319
320 return themeImagesPath;
321 }
322
323 private static boolean _isThemeCssFastLoad(
324 HttpServletRequest request, ThemeDisplay themeDisplay) {
325
326 if (themeDisplay != null) {
327 return themeDisplay.isThemeCssFastLoad();
328 }
329
330 return SessionParamUtil.getBoolean(
331 request, "css_fast_load", PropsValues.THEME_CSS_FAST_LOAD);
332 }
333
334 private static String _parseSass(
335 ServletContext servletContext, HttpServletRequest request,
336 ThemeDisplay themeDisplay, Theme theme, String resourcePath,
337 String content)
338 throws Exception {
339
340 Map<String, Object> inputObjects = new HashMap<String, Object>();
341
342 inputObjects.put("content", content);
343 inputObjects.put("cssRealPath", resourcePath);
344 inputObjects.put(
345 "cssThemePath", _getCssThemePath(request, themeDisplay, theme));
346
347 File sassTempDir = _getSassTempDir(servletContext);
348
349 inputObjects.put("sassCachePath", sassTempDir.getCanonicalPath());
350
351 UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
352 new UnsyncByteArrayOutputStream();
353
354 UnsyncPrintWriter unsyncPrintWriter = UnsyncPrintWriterPool.borrow(
355 unsyncByteArrayOutputStream);
356
357 inputObjects.put("out", unsyncPrintWriter);
358
359 _rubyExecutor.eval(null, inputObjects, null, _rubyScript);
360
361 unsyncPrintWriter.flush();
362
363 return unsyncByteArrayOutputStream.toString();
364 }
365
366
369 private static String propagateQueryString(
370 String content, String queryString) {
371
372 StringBuilder sb = new StringBuilder(content.length());
373
374 int pos = 0;
375
376 while (true) {
377 int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
378 int importY = content.indexOf(
379 _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
380
381 if ((importX == -1) || (importY == -1)) {
382 sb.append(content.substring(pos));
383
384 break;
385 }
386
387 sb.append(content.substring(pos, importY));
388 sb.append(CharPool.QUESTION);
389 sb.append(queryString);
390 sb.append(_CSS_IMPORT_END);
391
392 pos = importY + _CSS_IMPORT_END.length();
393 }
394
395 return sb.toString();
396 }
397
398 private static final String _CSS_IMPORT_BEGIN = "@import url(";
399
400 private static final String _CSS_IMPORT_END = ");";
401
402 private static final String _SASS_DIR = "sass";
403
404 private static final String _SASS_DIR_KEY =
405 DynamicCSSUtil.class.getName() + "#sass";
406
407 private static Log _log = LogFactoryUtil.getLog(DynamicCSSUtil.class);
408
409 private static Pattern _pluginThemePattern = Pattern.compile(
410 "\\/([^\\/]+)-theme\\/", Pattern.CASE_INSENSITIVE);
411 private static Pattern _portalThemePattern = Pattern.compile(
412 "themes\\/([^\\/]+)\\/css", Pattern.CASE_INSENSITIVE);
413 private static RubyExecutor _rubyExecutor = new RubyExecutor();
414 private static String _rubyScript;
415
416 }