001    /**
002     * Copyright (c) 2000-present 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;
016    
017    import com.liferay.portal.kernel.cache.PortalCache;
018    import com.liferay.portal.kernel.cache.SingleVMPoolUtil;
019    import com.liferay.portal.kernel.language.LanguageUtil;
020    import com.liferay.portal.kernel.log.Log;
021    import com.liferay.portal.kernel.log.LogFactoryUtil;
022    import com.liferay.portal.kernel.servlet.HttpHeaders;
023    import com.liferay.portal.kernel.servlet.ServletResponseUtil;
024    import com.liferay.portal.kernel.util.CharPool;
025    import com.liferay.portal.kernel.util.ContentTypes;
026    import com.liferay.portal.kernel.util.FileUtil;
027    import com.liferay.portal.kernel.util.HttpUtil;
028    import com.liferay.portal.kernel.util.LocaleUtil;
029    import com.liferay.portal.kernel.util.ParamUtil;
030    import com.liferay.portal.kernel.util.PropsKeys;
031    import com.liferay.portal.kernel.util.SetUtil;
032    import com.liferay.portal.kernel.util.StringPool;
033    import com.liferay.portal.kernel.util.StringUtil;
034    import com.liferay.portal.kernel.util.Time;
035    import com.liferay.portal.kernel.util.Validator;
036    import com.liferay.portal.language.AggregateResourceBundle;
037    import com.liferay.portal.language.LanguageResources;
038    import com.liferay.portal.minifier.MinifierUtil;
039    import com.liferay.portal.model.Portlet;
040    import com.liferay.portal.model.PortletApp;
041    import com.liferay.portal.service.PortletLocalServiceUtil;
042    import com.liferay.portal.servlet.filters.dynamiccss.DynamicCSSUtil;
043    import com.liferay.portal.util.AggregateUtil;
044    import com.liferay.portal.util.PortalUtil;
045    import com.liferay.portal.util.PortletKeys;
046    import com.liferay.portal.util.PrefsPropsUtil;
047    import com.liferay.portal.util.PropsValues;
048    import com.liferay.portlet.PortletConfigFactoryUtil;
049    
050    import java.io.IOException;
051    import java.io.Serializable;
052    
053    import java.net.HttpURLConnection;
054    import java.net.URL;
055    import java.net.URLConnection;
056    
057    import java.util.Arrays;
058    import java.util.Collections;
059    import java.util.Enumeration;
060    import java.util.LinkedHashSet;
061    import java.util.Locale;
062    import java.util.Map;
063    import java.util.ResourceBundle;
064    import java.util.Set;
065    
066    import javax.portlet.PortletConfig;
067    
068    import javax.servlet.ServletContext;
069    import javax.servlet.ServletException;
070    import javax.servlet.http.HttpServlet;
071    import javax.servlet.http.HttpServletRequest;
072    import javax.servlet.http.HttpServletResponse;
073    
074    /**
075     * @author Eduardo Lundgren
076     * @author Edward Han
077     * @author Zsigmond Rab
078     * @author Raymond Aug??
079     */
080    public class ComboServlet extends HttpServlet {
081    
082            public static void clearCache() {
083                    _bytesArrayPortalCache.removeAll();
084                    _fileContentBagPortalCache.removeAll();
085            }
086    
087            @Override
088            public void service(
089                            HttpServletRequest request, HttpServletResponse response)
090                    throws IOException, ServletException {
091    
092                    try {
093                            doService(request, response);
094                    }
095                    catch (Exception e) {
096                            _log.error(e, e);
097    
098                            PortalUtil.sendError(
099                                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e, request,
100                                    response);
101                    }
102            }
103    
104            protected static String getModulePortletId(String modulePath) {
105                    int index = modulePath.indexOf(CharPool.COLON);
106    
107                    if (index > 0) {
108                            return modulePath.substring(0, index);
109                    }
110    
111                    return PortletKeys.PORTAL;
112            }
113    
114            protected static String getResourcePath(String modulePath) {
115                    int index = modulePath.indexOf(CharPool.COLON);
116    
117                    if (index > 0) {
118                            return modulePath.substring(index + 1);
119                    }
120    
121                    return modulePath;
122            }
123    
124            protected void doService(
125                            HttpServletRequest request, HttpServletResponse response)
126                    throws Exception {
127    
128                    Set<String> modulePathsSet = new LinkedHashSet<String>();
129    
130                    Map<String, String[]> parameterMap = HttpUtil.getParameterMap(
131                            request.getQueryString());
132    
133                    Enumeration<String> enu = Collections.enumeration(
134                            parameterMap.keySet());
135    
136                    while (enu.hasMoreElements()) {
137                            String name = enu.nextElement();
138    
139                            if (_protectedParameters.contains(name)) {
140                                    continue;
141                            }
142    
143                            name = HttpUtil.decodePath(name);
144    
145                            modulePathsSet.add(name);
146                    }
147    
148                    if (modulePathsSet.isEmpty()) {
149                            response.sendError(
150                                    HttpServletResponse.SC_BAD_REQUEST,
151                                    "Modules paths set is empty");
152    
153                            return;
154                    }
155    
156                    String[] modulePaths = modulePathsSet.toArray(
157                            new String[modulePathsSet.size()]);
158    
159                    String firstModulePath = modulePaths[0];
160    
161                    String extension = FileUtil.getExtension(firstModulePath);
162    
163                    String minifierType = ParamUtil.getString(request, "minifierType");
164    
165                    if (Validator.isNull(minifierType)) {
166                            minifierType = "js";
167    
168                            if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) {
169                                    minifierType = "css";
170                            }
171                    }
172    
173                    if (!minifierType.equals("css") && !minifierType.equals("js")) {
174                            minifierType = "js";
175                    }
176    
177                    String modulePathsString = null;
178    
179                    byte[][] bytesArray = null;
180    
181                    if (!PropsValues.COMBO_CHECK_TIMESTAMP) {
182                            modulePathsString = Arrays.toString(modulePaths);
183    
184                            if (minifierType.equals("css") &&
185                                    PortalUtil.isRightToLeft(request)) {
186    
187                                    modulePathsString += ".rtl";
188                            }
189    
190                            bytesArray = _bytesArrayPortalCache.get(modulePathsString);
191                    }
192    
193                    if (bytesArray == null) {
194                            bytesArray = new byte[modulePaths.length][];
195    
196                            for (int i = 0; i < modulePaths.length; i++) {
197                                    String modulePath = modulePaths[i];
198    
199                                    if (!validateModuleExtension(modulePath)) {
200                                            response.setHeader(
201                                                    HttpHeaders.CACHE_CONTROL,
202                                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
203                                            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
204    
205                                            return;
206                                    }
207    
208                                    byte[] bytes = new byte[0];
209    
210                                    if (Validator.isNotNull(modulePath)) {
211                                            URL url = getResourceURL(request, modulePath);
212    
213                                            if (url == null) {
214                                                    response.setHeader(
215                                                            HttpHeaders.CACHE_CONTROL,
216                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
217                                                    response.setStatus(HttpServletResponse.SC_NOT_FOUND);
218    
219                                                    return;
220                                            }
221    
222                                            bytes = getResourceContent(
223                                                    request, response, url, modulePath, minifierType);
224                                    }
225    
226                                    bytesArray[i] = bytes;
227                            }
228    
229                            if ((modulePathsString != null) &&
230                                    !PropsValues.COMBO_CHECK_TIMESTAMP) {
231    
232                                    _bytesArrayPortalCache.put(modulePathsString, bytesArray);
233                            }
234                    }
235    
236                    String contentType = ContentTypes.TEXT_JAVASCRIPT;
237    
238                    if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) {
239                            contentType = ContentTypes.TEXT_CSS;
240                    }
241    
242                    response.setContentType(contentType);
243    
244                    ServletResponseUtil.write(response, bytesArray);
245            }
246    
247            protected byte[] getResourceContent(
248                            HttpServletRequest request, HttpServletResponse response,
249                            URL resourceURL, String modulePath, String minifierType)
250                    throws IOException {
251    
252                    String resourcePath = getResourcePath(modulePath);
253    
254                    String portletId = getModulePortletId(modulePath);
255    
256                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
257    
258                    if (!resourcePath.startsWith(portlet.getContextPath())) {
259                            resourcePath = portlet.getContextPath() + resourcePath;
260                    }
261    
262                    String fileContentKey = resourcePath.concat(StringPool.QUESTION).concat(
263                            minifierType);
264    
265                    FileContentBag fileContentBag = _fileContentBagPortalCache.get(
266                            fileContentKey);
267    
268                    if ((fileContentBag != null) && !PropsValues.COMBO_CHECK_TIMESTAMP) {
269                            return fileContentBag._fileContent;
270                    }
271    
272                    URLConnection urlConnection = null;
273    
274                    if (resourceURL != null) {
275                            urlConnection = resourceURL.openConnection();
276                    }
277    
278                    if ((fileContentBag != null) && PropsValues.COMBO_CHECK_TIMESTAMP) {
279                            long elapsedTime =
280                                    System.currentTimeMillis() - fileContentBag._lastModified;
281    
282                            if ((urlConnection != null) &&
283                                    (elapsedTime <= PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL) &&
284                                    (urlConnection.getLastModified() ==
285                                            fileContentBag._lastModified)) {
286    
287                                    return fileContentBag._fileContent;
288                            }
289    
290                            _fileContentBagPortalCache.remove(fileContentKey);
291                    }
292    
293                    if (resourceURL == null) {
294                            fileContentBag = _EMPTY_FILE_CONTENT_BAG;
295                    }
296                    else {
297                            String stringFileContent = StringUtil.read(
298                                    urlConnection.getInputStream());
299    
300                            if (!StringUtil.endsWith(resourcePath, _CSS_MINIFIED_SUFFIX) &&
301                                    !StringUtil.endsWith(
302                                            resourcePath, _JAVASCRIPT_MINIFIED_SUFFIX)) {
303    
304                                    if (minifierType.equals("css")) {
305                                            try {
306                                                    stringFileContent = DynamicCSSUtil.parseSass(
307                                                            getServletContext(), request, resourcePath,
308                                                            stringFileContent);
309                                            }
310                                            catch (Exception e) {
311                                                    _log.error(
312                                                            "Unable to parse SASS on CSS " +
313                                                                    resourceURL.getPath(), e);
314    
315                                                    if (_log.isDebugEnabled()) {
316                                                            _log.debug(stringFileContent);
317                                                    }
318    
319                                                    response.setHeader(
320                                                            HttpHeaders.CACHE_CONTROL,
321                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
322                                            }
323    
324                                            String baseURL = StringPool.BLANK;
325    
326                                            int slashIndex = resourcePath.lastIndexOf(CharPool.SLASH);
327    
328                                            if (slashIndex != -1) {
329                                                    baseURL = resourcePath.substring(0, slashIndex + 1);
330                                            }
331    
332                                            stringFileContent = AggregateUtil.updateRelativeURLs(
333                                                    stringFileContent, baseURL);
334    
335                                            stringFileContent = MinifierUtil.minifyCss(
336                                                    stringFileContent);
337                                    }
338                                    else if (minifierType.equals("js")) {
339                                            stringFileContent = translate(
340                                                    request, portletId, stringFileContent);
341    
342                                            stringFileContent = MinifierUtil.minifyJavaScript(
343                                                    resourcePath, stringFileContent);
344                                    }
345                            }
346    
347                            fileContentBag = new FileContentBag(
348                                    stringFileContent.getBytes(StringPool.UTF8),
349                                    urlConnection.getLastModified());
350                    }
351    
352                    if (PropsValues.COMBO_CHECK_TIMESTAMP) {
353                            int timeToLive =
354                                    (int)(PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL / Time.SECOND);
355    
356                            _fileContentBagPortalCache.put(
357                                    fileContentKey, fileContentBag, timeToLive);
358                    }
359    
360                    return fileContentBag._fileContent;
361            }
362    
363            protected URL getResourceURL(HttpServletRequest request, String modulePath)
364                    throws Exception {
365    
366                    String portletId = getModulePortletId(modulePath);
367    
368                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
369    
370                    if (portlet.isUndeployedPortlet()) {
371                            return null;
372                    }
373    
374                    PortletApp portletApp = portlet.getPortletApp();
375    
376                    ServletContext servletContext = portletApp.getServletContext();
377    
378                    String resourcePath = getResourcePath(modulePath);
379    
380                    String contextPath = servletContext.getContextPath();
381    
382                    if (resourcePath.startsWith(contextPath)) {
383                            resourcePath = resourcePath.substring(contextPath.length());
384                    }
385    
386                    URL url = servletContext.getResource(resourcePath);
387    
388                    if (url != null) {
389                            return url;
390                    }
391    
392                    url = new URL(
393                            request.getScheme(), request.getLocalAddr(), request.getLocalPort(),
394                            contextPath + resourcePath);
395    
396                    HttpURLConnection urlConnection =
397                            (HttpURLConnection)url.openConnection();
398    
399                    if (urlConnection.getResponseCode() == HttpServletResponse.SC_OK) {
400                            return url;
401                    }
402    
403                    throw new ServletException(
404                            "Resource " + resourcePath + " does not exist in " +
405                                    portlet.getContextPath());
406            }
407    
408            protected String translate(
409                    HttpServletRequest request, String portletId,
410                    String stringFileContent) {
411    
412                    String languageId = LanguageUtil.getLanguageId(request);
413    
414                    Locale locale = LocaleUtil.fromLanguageId(languageId);
415    
416                    ResourceBundle resourceBundle = LanguageResources.getResourceBundle(
417                            locale);
418    
419                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
420    
421                    if (portlet != null) {
422                            PortletConfig portletConfig = PortletConfigFactoryUtil.create(
423                                    portlet, getServletContext());
424    
425                            if (portletConfig != null) {
426                                    resourceBundle = new AggregateResourceBundle(
427                                            portletConfig.getResourceBundle(locale), resourceBundle);
428                            }
429                    }
430    
431                    return LanguageUtil.process(resourceBundle, locale, stringFileContent);
432            }
433    
434            protected boolean validateModuleExtension(String moduleName)
435                    throws Exception {
436    
437                    int index = moduleName.indexOf(CharPool.QUESTION);
438    
439                    if (index != -1) {
440                            moduleName = moduleName.substring(0, index);
441                    }
442    
443                    boolean validModuleExtension = false;
444    
445                    String[] fileExtensions = PrefsPropsUtil.getStringArray(
446                            PropsKeys.COMBO_ALLOWED_FILE_EXTENSIONS, StringPool.COMMA);
447    
448                    for (String fileExtension : fileExtensions) {
449                            if (StringPool.STAR.equals(fileExtension) ||
450                                    StringUtil.endsWith(moduleName, fileExtension)) {
451    
452                                    validModuleExtension = true;
453    
454                                    break;
455                            }
456                    }
457    
458                    return validModuleExtension;
459            }
460    
461            private static final String _CSS_EXTENSION = "css";
462    
463            private static final String _CSS_MINIFIED_SUFFIX = "-min.css";
464    
465            private static final FileContentBag _EMPTY_FILE_CONTENT_BAG =
466                    new FileContentBag(new byte[0], 0);
467    
468            private static final String _JAVASCRIPT_MINIFIED_SUFFIX = "-min.js";
469    
470            private static Log _log = LogFactoryUtil.getLog(ComboServlet.class);
471    
472            private static PortalCache<String, byte[][]> _bytesArrayPortalCache =
473                    SingleVMPoolUtil.getCache(ComboServlet.class.getName());
474            private static PortalCache<String, FileContentBag>
475                    _fileContentBagPortalCache = SingleVMPoolUtil.getCache(
476                            FileContentBag.class.getName());
477    
478            private Set<String> _protectedParameters = SetUtil.fromArray(
479                    new String[] {
480                            "b", "browserId", "minifierType", "languageId", "t", "themeId"
481                    });
482    
483            private static class FileContentBag implements Serializable {
484    
485                    public FileContentBag(byte[] fileContent, long lastModifiedTime) {
486                            _fileContent = fileContent;
487                            _lastModified = lastModifiedTime;
488                    }
489    
490                    private byte[] _fileContent;
491                    private long _lastModified;
492    
493            }
494    
495    }