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