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