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