001    /**
002     * Copyright (c) 2000-2013 Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
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    /**
058     * @author Raymond Augé
059     * @author Sergio Sánchez
060     */
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                    // Request will only be null when called by StripFilterTest
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            /**
380             * @see {@link AggregateFilter#aggregateCss(String, String)}
381             */
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    }