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;
016    
017    import com.liferay.portal.kernel.cache.PortalCache;
018    import com.liferay.portal.kernel.cache.SingleVMPoolUtil;
019    import com.liferay.portal.kernel.log.Log;
020    import com.liferay.portal.kernel.log.LogFactoryUtil;
021    import com.liferay.portal.kernel.servlet.HttpHeaders;
022    import com.liferay.portal.kernel.servlet.ServletContextUtil;
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.ParamUtil;
029    import com.liferay.portal.kernel.util.PropsKeys;
030    import com.liferay.portal.kernel.util.ServerDetector;
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.servlet.filters.dynamiccss.DynamicCSSUtil;
037    import com.liferay.portal.util.AggregateUtil;
038    import com.liferay.portal.util.MinifierUtil;
039    import com.liferay.portal.util.PortalUtil;
040    import com.liferay.portal.util.PrefsPropsUtil;
041    import com.liferay.portal.util.PropsValues;
042    
043    import java.io.IOException;
044    import java.io.Serializable;
045    
046    import java.net.URL;
047    import java.net.URLConnection;
048    
049    import java.util.Arrays;
050    import java.util.Collections;
051    import java.util.Enumeration;
052    import java.util.LinkedHashSet;
053    import java.util.Map;
054    import java.util.Set;
055    
056    import javax.servlet.ServletContext;
057    import javax.servlet.ServletException;
058    import javax.servlet.http.HttpServlet;
059    import javax.servlet.http.HttpServletRequest;
060    import javax.servlet.http.HttpServletResponse;
061    
062    /**
063     * @author Eduardo Lundgren
064     * @author Edward Han
065     * @author Zsigmond Rab
066     * @author Raymond Aug??
067     */
068    public class ComboServlet extends HttpServlet {
069    
070            @Override
071            public void service(
072                            HttpServletRequest request, HttpServletResponse response)
073                    throws IOException, ServletException {
074    
075                    try {
076                            doService(request, response);
077                    }
078                    catch (Exception e) {
079                            _log.error(e, e);
080    
081                            PortalUtil.sendError(
082                                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e, request,
083                                    response);
084                    }
085            }
086    
087            protected void doService(
088                            HttpServletRequest request, HttpServletResponse response)
089                    throws Exception {
090    
091                    Set<String> modulePathsSet = new LinkedHashSet<String>();
092    
093                    Enumeration<String> enu = request.getParameterNames();
094    
095                    if (ServerDetector.isWebSphere()) {
096                            Map<String, String[]> parameterMap = HttpUtil.getParameterMap(
097                                    request.getQueryString());
098    
099                            enu = Collections.enumeration(parameterMap.keySet());
100                    }
101    
102                    while (enu.hasMoreElements()) {
103                            String name = enu.nextElement();
104    
105                            if (_protectedParameters.contains(name)) {
106                                    continue;
107                            }
108    
109                            modulePathsSet.add(name);
110                    }
111    
112                    if (modulePathsSet.size() == 0) {
113                            response.sendError(
114                                    HttpServletResponse.SC_BAD_REQUEST,
115                                    "Modules paths set is empty");
116    
117                            return;
118                    }
119    
120                    String[] modulePaths = modulePathsSet.toArray(
121                            new String[modulePathsSet.size()]);
122    
123                    String modulePathsString = null;
124    
125                    byte[][] bytesArray = null;
126    
127                    if (!PropsValues.COMBO_CHECK_TIMESTAMP) {
128                            modulePathsString = Arrays.toString(modulePaths);
129    
130                            bytesArray = _bytesArrayPortalCache.get(modulePathsString);
131                    }
132    
133                    String firstModulePath = modulePaths[0];
134    
135                    String extension = FileUtil.getExtension(firstModulePath);
136    
137                    if (bytesArray == null) {
138                            ServletContext servletContext = getServletContext();
139    
140                            String rootPath = ServletContextUtil.getRootPath(servletContext);
141    
142                            String minifierType = ParamUtil.getString(request, "minifierType");
143    
144                            if (Validator.isNull(minifierType)) {
145                                    minifierType = "js";
146    
147                                    if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) {
148                                            minifierType = "css";
149                                    }
150                            }
151    
152                            if (!minifierType.equals("css") && !minifierType.equals("js")) {
153                                    minifierType = "js";
154                            }
155    
156                            bytesArray = new byte[modulePaths.length][];
157    
158                            for (int i = 0; i < modulePaths.length; i++) {
159                                    String modulePath = modulePaths[i];
160    
161                                    if (!validateModuleExtension(modulePath)) {
162                                            response.setHeader(
163                                                    HttpHeaders.CACHE_CONTROL,
164                                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
165                                            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
166    
167                                            return;
168                                    }
169    
170                                    byte[] bytes = new byte[0];
171    
172                                    if (Validator.isNotNull(modulePath)) {
173                                            modulePath = StringUtil.replaceFirst(
174                                                    modulePath, PortalUtil.getPathContext(),
175                                                    StringPool.BLANK);
176    
177                                            URL url = getResourceURL(
178                                                    servletContext, rootPath, modulePath);
179    
180                                            if (url == null) {
181                                                    response.setHeader(
182                                                            HttpHeaders.CACHE_CONTROL,
183                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
184                                                    response.setStatus(HttpServletResponse.SC_NOT_FOUND);
185    
186                                                    return;
187                                            }
188    
189                                            bytes = getResourceContent(
190                                                    request, response, url, modulePath, minifierType);
191                                    }
192    
193                                    bytesArray[i] = bytes;
194                            }
195    
196                            if ((modulePathsString != null) &&
197                                    !PropsValues.COMBO_CHECK_TIMESTAMP) {
198    
199                                    _bytesArrayPortalCache.put(modulePathsString, bytesArray);
200                            }
201                    }
202    
203                    String contentType = ContentTypes.TEXT_JAVASCRIPT;
204    
205                    if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) {
206                            contentType = ContentTypes.TEXT_CSS;
207                    }
208    
209                    response.setContentType(contentType);
210    
211                    ServletResponseUtil.write(response, bytesArray);
212            }
213    
214            protected byte[] getResourceContent(
215                            HttpServletRequest request, HttpServletResponse response,
216                            URL resourceURL, String resourcePath, String minifierType)
217                    throws IOException {
218    
219                    String fileContentKey = resourcePath.concat(StringPool.QUESTION).concat(
220                            minifierType);
221    
222                    FileContentBag fileContentBag = _fileContentBagPortalCache.get(
223                            fileContentKey);
224    
225                    if ((fileContentBag != null) && !PropsValues.COMBO_CHECK_TIMESTAMP) {
226                            return fileContentBag._fileContent;
227                    }
228    
229                    URLConnection urlConnection = null;
230    
231                    if (resourceURL != null) {
232                            urlConnection = resourceURL.openConnection();
233                    }
234    
235                    if ((fileContentBag != null) && PropsValues.COMBO_CHECK_TIMESTAMP) {
236                            long elapsedTime =
237                                    System.currentTimeMillis() - fileContentBag._lastModified;
238    
239                            if ((urlConnection != null) &&
240                                    (elapsedTime <= PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL) &&
241                                    (urlConnection.getLastModified() ==
242                                            fileContentBag._lastModified)) {
243    
244                                    return fileContentBag._fileContent;
245                            }
246    
247                            _fileContentBagPortalCache.remove(fileContentKey);
248                    }
249    
250                    if (resourceURL == null) {
251                            fileContentBag = _EMPTY_FILE_CONTENT_BAG;
252                    }
253                    else {
254                            String stringFileContent = StringUtil.read(
255                                    urlConnection.getInputStream());
256    
257                            if (!StringUtil.endsWith(resourcePath, _CSS_MINIFIED_SUFFIX) &&
258                                    !StringUtil.endsWith(
259                                            resourcePath, _JAVASCRIPT_MINIFIED_SUFFIX)) {
260    
261                                    if (minifierType.equals("css")) {
262                                            try {
263                                                    stringFileContent = DynamicCSSUtil.parseSass(
264                                                            getServletContext(), request, resourcePath,
265                                                            stringFileContent);
266                                            }
267                                            catch (Exception e) {
268                                                    _log.error(
269                                                            "Unable to parse SASS on CSS " +
270                                                                    resourceURL.getPath(), e);
271    
272                                                    if (_log.isDebugEnabled()) {
273                                                            _log.debug(stringFileContent);
274                                                    }
275    
276                                                    response.setHeader(
277                                                            HttpHeaders.CACHE_CONTROL,
278                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
279                                            }
280    
281                                            String baseURL = StringPool.BLANK;
282    
283                                            int index = resourcePath.lastIndexOf(CharPool.SLASH);
284    
285                                            if (index != -1) {
286                                                    baseURL = resourcePath.substring(0, index + 1);
287                                            }
288    
289                                            stringFileContent = AggregateUtil.updateRelativeURLs(
290                                                    stringFileContent, baseURL);
291    
292                                            stringFileContent = MinifierUtil.minifyCss(
293                                                    stringFileContent);
294                                    }
295                                    else if (minifierType.equals("js")) {
296                                            stringFileContent = MinifierUtil.minifyJavaScript(
297                                                    stringFileContent);
298                                    }
299                            }
300    
301                            fileContentBag = new FileContentBag(
302                                    stringFileContent.getBytes(StringPool.UTF8),
303                                    urlConnection.getLastModified());
304                    }
305    
306                    if (PropsValues.COMBO_CHECK_TIMESTAMP) {
307                            int timeToLive =
308                                    (int)(PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL / Time.SECOND);
309    
310                            _fileContentBagPortalCache.put(
311                                    fileContentKey, fileContentBag, timeToLive);
312                    }
313    
314                    return fileContentBag._fileContent;
315            }
316    
317            protected URL getResourceURL(
318                            ServletContext servletContext, String rootPath, String path)
319                    throws Exception {
320    
321                    URL url = servletContext.getResource(path);
322    
323                    if (url == null) {
324                            return null;
325                    }
326    
327                    String filePath = ServletContextUtil.getResourcePath(url);
328    
329                    int pos = filePath.indexOf(
330                            rootPath.concat(StringPool.SLASH).concat(_JAVASCRIPT_DIR));
331    
332                    if (pos == 0) {
333                            return url;
334                    }
335    
336                    return null;
337            }
338    
339            protected boolean validateModuleExtension(String moduleName)
340                    throws Exception {
341    
342                    boolean validModuleExtension = false;
343    
344                    String[] fileExtensions = PrefsPropsUtil.getStringArray(
345                            PropsKeys.COMBO_ALLOWED_FILE_EXTENSIONS, StringPool.COMMA);
346    
347                    for (String fileExtension : fileExtensions) {
348                            if (StringPool.STAR.equals(fileExtension) ||
349                                    StringUtil.endsWith(moduleName, fileExtension)) {
350    
351                                    validModuleExtension = true;
352    
353                                    break;
354                            }
355                    }
356    
357                    return validModuleExtension;
358            }
359    
360            private static final String _CSS_EXTENSION = "css";
361    
362            private static final String _CSS_MINIFIED_SUFFIX = "-min.css";
363    
364            private static final FileContentBag _EMPTY_FILE_CONTENT_BAG =
365                    new FileContentBag(new byte[0], 0);
366    
367            private static final String _JAVASCRIPT_DIR = "html/js";
368    
369            private static final String _JAVASCRIPT_MINIFIED_SUFFIX = "-min.js";
370    
371            private static Log _log = LogFactoryUtil.getLog(ComboServlet.class);
372    
373            private PortalCache<String, byte[][]> _bytesArrayPortalCache =
374                    SingleVMPoolUtil.getCache(ComboServlet.class.getName());
375            private PortalCache<String, FileContentBag> _fileContentBagPortalCache =
376                    SingleVMPoolUtil.getCache(FileContentBag.class.getName());
377            private Set<String> _protectedParameters = SetUtil.fromArray(
378                    new String[] {"b", "browserId", "minifierType", "languageId", "t"});
379    
380            private static class FileContentBag implements Serializable {
381    
382                    public FileContentBag(byte[] fileContent, long lastModifiedTime) {
383                            _fileContent = fileContent;
384                            _lastModified = lastModifiedTime;
385                    }
386    
387                    private byte[] _fileContent;
388                    private long _lastModified;
389    
390            }
391    
392    }