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                            else if (minifierType.equals("js")) {
190                                    modulePathsString +=
191                                            StringPool.POUND + LanguageUtil.getLanguageId(request);
192                            }
193    
194                            bytesArray = _bytesArrayPortalCache.get(modulePathsString);
195                    }
196    
197                    if (bytesArray == null) {
198                            bytesArray = new byte[modulePaths.length][];
199    
200                            for (int i = 0; i < modulePaths.length; i++) {
201                                    String modulePath = modulePaths[i];
202    
203                                    if (!validateModuleExtension(modulePath)) {
204                                            response.setHeader(
205                                                    HttpHeaders.CACHE_CONTROL,
206                                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
207                                            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
208    
209                                            return;
210                                    }
211    
212                                    byte[] bytes = new byte[0];
213    
214                                    if (Validator.isNotNull(modulePath)) {
215                                            URL url = getResourceURL(request, modulePath);
216    
217                                            if (url == null) {
218                                                    response.setHeader(
219                                                            HttpHeaders.CACHE_CONTROL,
220                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
221                                                    response.setStatus(HttpServletResponse.SC_NOT_FOUND);
222    
223                                                    return;
224                                            }
225    
226                                            bytes = getResourceContent(
227                                                    request, response, url, modulePath, minifierType);
228                                    }
229    
230                                    bytesArray[i] = bytes;
231                            }
232    
233                            if ((modulePathsString != null) &&
234                                    !PropsValues.COMBO_CHECK_TIMESTAMP) {
235    
236                                    _bytesArrayPortalCache.put(modulePathsString, bytesArray);
237                            }
238                    }
239    
240                    String contentType = ContentTypes.TEXT_JAVASCRIPT;
241    
242                    if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) {
243                            contentType = ContentTypes.TEXT_CSS;
244                    }
245    
246                    response.setContentType(contentType);
247    
248                    ServletResponseUtil.write(response, bytesArray);
249            }
250    
251            protected byte[] getResourceContent(
252                            HttpServletRequest request, HttpServletResponse response,
253                            URL resourceURL, String modulePath, String minifierType)
254                    throws IOException {
255    
256                    String resourcePath = getResourcePath(modulePath);
257    
258                    String portletId = getModulePortletId(modulePath);
259    
260                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
261    
262                    if (!resourcePath.startsWith(portlet.getContextPath())) {
263                            resourcePath = portlet.getContextPath() + resourcePath;
264                    }
265    
266                    String fileContentKey = resourcePath.concat(StringPool.QUESTION).concat(
267                            minifierType);
268    
269                    FileContentBag fileContentBag = _fileContentBagPortalCache.get(
270                            fileContentKey);
271    
272                    if ((fileContentBag != null) && !PropsValues.COMBO_CHECK_TIMESTAMP) {
273                            return fileContentBag._fileContent;
274                    }
275    
276                    URLConnection urlConnection = null;
277    
278                    if (resourceURL != null) {
279                            urlConnection = resourceURL.openConnection();
280                    }
281    
282                    if ((fileContentBag != null) && PropsValues.COMBO_CHECK_TIMESTAMP) {
283                            long elapsedTime =
284                                    System.currentTimeMillis() - fileContentBag._lastModified;
285    
286                            if ((urlConnection != null) &&
287                                    (elapsedTime <= PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL) &&
288                                    (urlConnection.getLastModified() ==
289                                            fileContentBag._lastModified)) {
290    
291                                    return fileContentBag._fileContent;
292                            }
293    
294                            _fileContentBagPortalCache.remove(fileContentKey);
295                    }
296    
297                    if (resourceURL == null) {
298                            fileContentBag = _EMPTY_FILE_CONTENT_BAG;
299                    }
300                    else {
301                            String stringFileContent = StringUtil.read(
302                                    urlConnection.getInputStream());
303    
304                            if (!StringUtil.endsWith(resourcePath, _CSS_MINIFIED_SUFFIX) &&
305                                    !StringUtil.endsWith(
306                                            resourcePath, _JAVASCRIPT_MINIFIED_SUFFIX)) {
307    
308                                    if (minifierType.equals("css")) {
309                                            try {
310                                                    stringFileContent = DynamicCSSUtil.parseSass(
311                                                            getServletContext(), request, resourcePath,
312                                                            stringFileContent);
313                                            }
314                                            catch (Exception e) {
315                                                    _log.error(
316                                                            "Unable to parse SASS on CSS " +
317                                                                    resourceURL.getPath(), e);
318    
319                                                    if (_log.isDebugEnabled()) {
320                                                            _log.debug(stringFileContent);
321                                                    }
322    
323                                                    response.setHeader(
324                                                            HttpHeaders.CACHE_CONTROL,
325                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
326                                            }
327    
328                                            String baseURL = StringPool.BLANK;
329    
330                                            int slashIndex = resourcePath.lastIndexOf(CharPool.SLASH);
331    
332                                            if (slashIndex != -1) {
333                                                    baseURL = resourcePath.substring(0, slashIndex + 1);
334                                            }
335    
336                                            stringFileContent = AggregateUtil.updateRelativeURLs(
337                                                    stringFileContent, baseURL);
338    
339                                            stringFileContent = MinifierUtil.minifyCss(
340                                                    stringFileContent);
341                                    }
342                                    else if (minifierType.equals("js")) {
343                                            stringFileContent = translate(
344                                                    request, portletId, stringFileContent);
345    
346                                            stringFileContent = MinifierUtil.minifyJavaScript(
347                                                    resourcePath, stringFileContent);
348                                    }
349                            }
350    
351                            fileContentBag = new FileContentBag(
352                                    stringFileContent.getBytes(StringPool.UTF8),
353                                    urlConnection.getLastModified());
354                    }
355    
356                    if (PropsValues.COMBO_CHECK_TIMESTAMP) {
357                            int timeToLive =
358                                    (int)(PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL / Time.SECOND);
359    
360                            _fileContentBagPortalCache.put(
361                                    fileContentKey, fileContentBag, timeToLive);
362                    }
363    
364                    return fileContentBag._fileContent;
365            }
366    
367            protected URL getResourceURL(HttpServletRequest request, String modulePath)
368                    throws Exception {
369    
370                    String portletId = getModulePortletId(modulePath);
371    
372                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
373    
374                    if (portlet.isUndeployedPortlet()) {
375                            return null;
376                    }
377    
378                    PortletApp portletApp = portlet.getPortletApp();
379    
380                    ServletContext servletContext = portletApp.getServletContext();
381    
382                    String resourcePath = getResourcePath(modulePath);
383    
384                    String contextPath = servletContext.getContextPath();
385    
386                    if (resourcePath.startsWith(contextPath)) {
387                            resourcePath = resourcePath.substring(contextPath.length());
388                    }
389    
390                    URL url = servletContext.getResource(resourcePath);
391    
392                    if (url != null) {
393                            return url;
394                    }
395    
396                    url = new URL(
397                            request.getScheme(), request.getLocalAddr(), request.getLocalPort(),
398                            contextPath + resourcePath);
399    
400                    HttpURLConnection urlConnection =
401                            (HttpURLConnection)url.openConnection();
402    
403                    if (urlConnection.getResponseCode() == HttpServletResponse.SC_OK) {
404                            return url;
405                    }
406    
407                    throw new ServletException(
408                            "Resource " + resourcePath + " does not exist in " +
409                                    portlet.getContextPath());
410            }
411    
412            protected String translate(
413                    HttpServletRequest request, String portletId,
414                    String stringFileContent) {
415    
416                    String languageId = LanguageUtil.getLanguageId(request);
417    
418                    Locale locale = LocaleUtil.fromLanguageId(languageId);
419    
420                    ResourceBundle resourceBundle = LanguageResources.getResourceBundle(
421                            locale);
422    
423                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
424    
425                    if (portlet != null) {
426                            PortletConfig portletConfig = PortletConfigFactoryUtil.create(
427                                    portlet, getServletContext());
428    
429                            if (portletConfig != null) {
430                                    resourceBundle = new AggregateResourceBundle(
431                                            portletConfig.getResourceBundle(locale), resourceBundle);
432                            }
433                    }
434    
435                    return LanguageUtil.process(resourceBundle, locale, stringFileContent);
436            }
437    
438            protected boolean validateModuleExtension(String moduleName)
439                    throws Exception {
440    
441                    int index = moduleName.indexOf(CharPool.QUESTION);
442    
443                    if (index != -1) {
444                            moduleName = moduleName.substring(0, index);
445                    }
446    
447                    boolean validModuleExtension = false;
448    
449                    String[] fileExtensions = PrefsPropsUtil.getStringArray(
450                            PropsKeys.COMBO_ALLOWED_FILE_EXTENSIONS, StringPool.COMMA);
451    
452                    for (String fileExtension : fileExtensions) {
453                            if (StringPool.STAR.equals(fileExtension) ||
454                                    StringUtil.endsWith(moduleName, fileExtension)) {
455    
456                                    validModuleExtension = true;
457    
458                                    break;
459                            }
460                    }
461    
462                    return validModuleExtension;
463            }
464    
465            private static final String _CSS_EXTENSION = "css";
466    
467            private static final String _CSS_MINIFIED_SUFFIX = "-min.css";
468    
469            private static final FileContentBag _EMPTY_FILE_CONTENT_BAG =
470                    new FileContentBag(new byte[0], 0);
471    
472            private static final String _JAVASCRIPT_MINIFIED_SUFFIX = "-min.js";
473    
474            private static Log _log = LogFactoryUtil.getLog(ComboServlet.class);
475    
476            private static PortalCache<String, byte[][]> _bytesArrayPortalCache =
477                    SingleVMPoolUtil.getCache(ComboServlet.class.getName());
478            private static PortalCache<String, FileContentBag>
479                    _fileContentBagPortalCache = SingleVMPoolUtil.getCache(
480                            FileContentBag.class.getName());
481    
482            private Set<String> _protectedParameters = SetUtil.fromArray(
483                    new String[] {
484                            "b", "browserId", "minifierType", "languageId", "t", "themeId"
485                    });
486    
487            private static class FileContentBag implements Serializable {
488    
489                    public FileContentBag(byte[] fileContent, long lastModifiedTime) {
490                            _fileContent = fileContent;
491                            _lastModified = lastModifiedTime;
492                    }
493    
494                    private byte[] _fileContent;
495                    private long _lastModified;
496    
497            }
498    
499    }