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