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