001    /**
002     * Copyright (c) 2000-2012 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.HashSet;
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 HashSet<String>(modulePaths.length);
094    
095                    for (String path : modulePaths) {
096                            modulePathsSet.add(path);
097                    }
098    
099                    modulePaths = modulePathsSet.toArray(new String[modulePathsSet.size()]);
100    
101                    Arrays.sort(modulePaths);
102    
103                    String modulePathsString = null;
104    
105                    byte[][] bytesArray = null;
106    
107                    if (!PropsValues.COMBO_CHECK_TIMESTAMP) {
108                            modulePathsString = Arrays.toString(modulePaths);
109    
110                            bytesArray = _bytesArrayPortalCache.get(modulePathsString);
111                    }
112    
113                    String firstModulePath = modulePaths[0];
114    
115                    String extension = FileUtil.getExtension(firstModulePath);
116    
117                    if (bytesArray == null) {
118                            ServletContext servletContext = getServletContext();
119    
120                            String rootPath = ServletContextUtil.getRootPath(servletContext);
121    
122                            String p = ParamUtil.getString(request, "p");
123    
124                            String minifierType = ParamUtil.getString(request, "minifierType");
125    
126                            if (Validator.isNull(minifierType)) {
127                                    minifierType = "js";
128    
129                                    if (extension.equalsIgnoreCase(_CSS_EXTENSION)) {
130                                            minifierType = "css";
131                                    }
132                            }
133    
134                            if (!minifierType.equals("css") && !minifierType.equals("js")) {
135                                    minifierType = "js";
136                            }
137    
138                            int length = modulePaths.length;
139    
140                            bytesArray = new byte[length][];
141    
142                            for (String modulePath : modulePaths) {
143                                    if (!validateModuleExtension(modulePath)) {
144                                            response.setHeader(
145                                                    HttpHeaders.CACHE_CONTROL,
146                                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
147                                            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
148    
149                                            return;
150                                    }
151    
152                                    byte[] bytes = new byte[0];
153    
154                                    if (Validator.isNotNull(modulePath)) {
155                                            modulePath = StringUtil.replaceFirst(
156                                                    p.concat(modulePath), contextPath, StringPool.BLANK);
157    
158                                            URL resourceURL = getResourceURL(
159                                                    servletContext, rootPath, modulePath);
160    
161                                            if (resourceURL == null) {
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                                            bytes = getResourceContent(
171                                                    request, response, resourceURL, modulePath,
172                                                    minifierType);
173                                    }
174    
175                                    bytesArray[--length] = bytes;
176                            }
177    
178                            if ((modulePathsString != null) &&
179                                    !PropsValues.COMBO_CHECK_TIMESTAMP) {
180    
181                                    _bytesArrayPortalCache.put(modulePathsString, bytesArray);
182                            }
183                    }
184    
185                    String contentType = ContentTypes.TEXT_JAVASCRIPT;
186    
187                    if (extension.equalsIgnoreCase(_CSS_EXTENSION)) {
188                            contentType = ContentTypes.TEXT_CSS;
189                    }
190    
191                    response.setContentType(contentType);
192    
193                    ServletResponseUtil.write(response, bytesArray);
194            }
195    
196            protected byte[] getResourceContent(
197                            HttpServletRequest request, HttpServletResponse response,
198                            URL resourceURL, String resourcePath, String minifierType)
199                    throws IOException {
200    
201                    String fileContentKey = resourcePath.concat(StringPool.QUESTION).concat(
202                            minifierType);
203    
204                    FileContentBag fileContentBag = _fileContentBagPortalCache.get(
205                            fileContentKey);
206    
207                    if ((fileContentBag != null) && !PropsValues.COMBO_CHECK_TIMESTAMP) {
208                            return fileContentBag._fileContent;
209                    }
210    
211                    URLConnection urlConnection = null;
212    
213                    if (resourceURL != null) {
214                            urlConnection = resourceURL.openConnection();
215                    }
216    
217                    if ((fileContentBag != null) && PropsValues.COMBO_CHECK_TIMESTAMP) {
218                            long elapsedTime =
219                                    System.currentTimeMillis() - fileContentBag._lastModified;
220    
221                            if ((urlConnection != null) &&
222                                    (elapsedTime <= PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL) &&
223                                    (urlConnection.getLastModified() ==
224                                            fileContentBag._lastModified)) {
225    
226                                    return fileContentBag._fileContent;
227                            }
228    
229                            _fileContentBagPortalCache.remove(fileContentKey);
230                    }
231    
232                    if (resourceURL == null) {
233                            fileContentBag = _EMPTY_FILE_CONTENT_BAG;
234                    }
235                    else {
236                            String stringFileContent = StringUtil.read(
237                                    urlConnection.getInputStream());
238    
239                            if (!StringUtil.endsWith(resourcePath, _CSS_MINIFIED_SUFFIX) &&
240                                    !StringUtil.endsWith(
241                                            resourcePath, _JAVASCRIPT_MINIFIED_SUFFIX)) {
242    
243                                    if (minifierType.equals("css")) {
244                                            try {
245                                                    stringFileContent = DynamicCSSUtil.parseSass(
246                                                            getServletContext(), request, resourcePath,
247                                                            stringFileContent);
248                                            }
249                                            catch (Exception e) {
250                                                    _log.error(
251                                                            "Unable to parse SASS on CSS " +
252                                                                    resourceURL.getPath(), e);
253    
254                                                    if (_log.isDebugEnabled()) {
255                                                            _log.debug(stringFileContent);
256                                                    }
257    
258                                                    response.setHeader(
259                                                            HttpHeaders.CACHE_CONTROL,
260                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
261                                            }
262    
263                                            stringFileContent = MinifierUtil.minifyCss(
264                                                    stringFileContent);
265                                    }
266                                    else if (minifierType.equals("js")) {
267                                            stringFileContent = MinifierUtil.minifyJavaScript(
268                                                    stringFileContent);
269                                    }
270                            }
271    
272                            fileContentBag = new FileContentBag(
273                                    stringFileContent.getBytes(StringPool.UTF8),
274                                    urlConnection.getLastModified());
275                    }
276    
277                    if (PropsValues.COMBO_CHECK_TIMESTAMP) {
278                            int timeToLive =
279                                    (int)(PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL / Time.SECOND);
280    
281                            _fileContentBagPortalCache.put(
282                                    fileContentKey, fileContentBag, timeToLive);
283                    }
284    
285                    return fileContentBag._fileContent;
286            }
287    
288            protected URL getResourceURL(
289                            ServletContext servletContext, String rootPath, String path)
290                    throws IOException {
291    
292                    URL resourceURL = servletContext.getResource(path);
293    
294                    if (resourceURL == null) {
295                            return null;
296                    }
297    
298                    String filePath = resourceURL.toString();
299    
300                    int pos = filePath.indexOf(
301                            rootPath.concat(StringPool.SLASH).concat(_JAVASCRIPT_DIR));
302    
303                    if (pos == 0) {
304                            return resourceURL;
305                    }
306    
307                    return null;
308            }
309    
310            protected boolean validateModuleExtension(String moduleName)
311                    throws Exception {
312    
313                    boolean validModuleExtension = false;
314    
315                    String[] fileExtensions = PrefsPropsUtil.getStringArray(
316                            PropsKeys.COMBO_ALLOWED_FILE_EXTENSIONS, StringPool.COMMA);
317    
318                    for (String fileExtension : fileExtensions) {
319                            if (StringPool.STAR.equals(fileExtension) ||
320                                    StringUtil.endsWith(moduleName, fileExtension)) {
321    
322                                    validModuleExtension = true;
323    
324                                    break;
325                            }
326                    }
327    
328                    return validModuleExtension;
329            }
330    
331            private static final String _CSS_EXTENSION = "css";
332    
333            private static final String _CSS_MINIFIED_SUFFIX = "-min.css";
334    
335            private static final FileContentBag _EMPTY_FILE_CONTENT_BAG =
336                    new FileContentBag(new byte[0], 0);
337    
338            private static final String _JAVASCRIPT_DIR = "html/js";
339    
340            private static final String _JAVASCRIPT_MINIFIED_SUFFIX = "-min.js";
341    
342            private static Log _log = LogFactoryUtil.getLog(ComboServlet.class);
343    
344            private PortalCache<String, byte[][]> _bytesArrayPortalCache =
345                    SingleVMPoolUtil.getCache(ComboServlet.class.getName());
346            private PortalCache<String, FileContentBag> _fileContentBagPortalCache =
347                    SingleVMPoolUtil.getCache(FileContentBag.class.getName());
348    
349            private static class FileContentBag implements Serializable {
350    
351                    public FileContentBag(byte[] fileContent, long lastModifiedTime) {
352                            _fileContent = fileContent;
353                            _lastModified = lastModifiedTime;
354                    }
355    
356                    private byte[] _fileContent;
357                    private long _lastModified;
358    
359            }
360    
361    }