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.NoSuchLayoutException;
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                            PortalUtil.sendError(
115                                    HttpServletResponse.SC_NOT_FOUND,
116                                    new NoSuchLayoutException(
117                                            "Query string translates to an empty module paths set"),
118                                    request, response);
119    
120                            return;
121                    }
122    
123                    String[] modulePaths = modulePathsSet.toArray(
124                            new String[modulePathsSet.size()]);
125    
126                    String firstModulePath = modulePaths[0];
127    
128                    String extension = FileUtil.getExtension(firstModulePath);
129    
130                    String minifierType = ParamUtil.getString(request, "minifierType");
131    
132                    if (Validator.isNull(minifierType)) {
133                            minifierType = "js";
134    
135                            if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) {
136                                    minifierType = "css";
137                            }
138                    }
139    
140                    if (!minifierType.equals("css") && !minifierType.equals("js")) {
141                            minifierType = "js";
142                    }
143    
144                    String modulePathsString = null;
145    
146                    byte[][] bytesArray = null;
147    
148                    if (!PropsValues.COMBO_CHECK_TIMESTAMP) {
149                            modulePathsString = Arrays.toString(modulePaths);
150    
151                            if (minifierType.equals("css") &&
152                                    PortalUtil.isRightToLeft(request)) {
153    
154                                    modulePathsString += ".rtl";
155                            }
156    
157                            bytesArray = _bytesArrayPortalCache.get(modulePathsString);
158                    }
159    
160                    if (bytesArray == null) {
161                            ServletContext servletContext = getServletContext();
162    
163                            String rootPath = ServletContextUtil.getRootPath(servletContext);
164    
165                            bytesArray = new byte[modulePaths.length][];
166    
167                            for (int i = 0; i < modulePaths.length; i++) {
168                                    String modulePath = modulePaths[i];
169    
170                                    if (!validateModuleExtension(modulePath)) {
171                                            response.setHeader(
172                                                    HttpHeaders.CACHE_CONTROL,
173                                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
174                                            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
175    
176                                            return;
177                                    }
178    
179                                    byte[] bytes = new byte[0];
180    
181                                    if (Validator.isNotNull(modulePath)) {
182                                            modulePath = StringUtil.replaceFirst(
183                                                    modulePath, PortalUtil.getPathContext(),
184                                                    StringPool.BLANK);
185    
186                                            URL url = getResourceURL(
187                                                    servletContext, rootPath, modulePath);
188    
189                                            if (url == null) {
190                                                    response.setHeader(
191                                                            HttpHeaders.CACHE_CONTROL,
192                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
193                                                    response.setStatus(HttpServletResponse.SC_NOT_FOUND);
194    
195                                                    return;
196                                            }
197    
198                                            bytes = getResourceContent(
199                                                    request, response, url, modulePath, minifierType);
200                                    }
201    
202                                    bytesArray[i] = bytes;
203                            }
204    
205                            if ((modulePathsString != null) &&
206                                    !PropsValues.COMBO_CHECK_TIMESTAMP) {
207    
208                                    _bytesArrayPortalCache.put(modulePathsString, bytesArray);
209                            }
210                    }
211    
212                    String contentType = ContentTypes.TEXT_JAVASCRIPT;
213    
214                    if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) {
215                            contentType = ContentTypes.TEXT_CSS;
216                    }
217    
218                    response.setContentType(contentType);
219    
220                    ServletResponseUtil.write(response, bytesArray);
221            }
222    
223            protected byte[] getResourceContent(
224                            HttpServletRequest request, HttpServletResponse response,
225                            URL resourceURL, String resourcePath, String minifierType)
226                    throws IOException {
227    
228                    String fileContentKey = resourcePath.concat(StringPool.QUESTION).concat(
229                            minifierType);
230    
231                    FileContentBag fileContentBag = _fileContentBagPortalCache.get(
232                            fileContentKey);
233    
234                    if ((fileContentBag != null) && !PropsValues.COMBO_CHECK_TIMESTAMP) {
235                            return fileContentBag._fileContent;
236                    }
237    
238                    URLConnection urlConnection = null;
239    
240                    if (resourceURL != null) {
241                            urlConnection = resourceURL.openConnection();
242                    }
243    
244                    if ((fileContentBag != null) && PropsValues.COMBO_CHECK_TIMESTAMP) {
245                            long elapsedTime =
246                                    System.currentTimeMillis() - fileContentBag._lastModified;
247    
248                            if ((urlConnection != null) &&
249                                    (elapsedTime <= PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL) &&
250                                    (urlConnection.getLastModified() ==
251                                            fileContentBag._lastModified)) {
252    
253                                    return fileContentBag._fileContent;
254                            }
255    
256                            _fileContentBagPortalCache.remove(fileContentKey);
257                    }
258    
259                    if (resourceURL == null) {
260                            fileContentBag = _EMPTY_FILE_CONTENT_BAG;
261                    }
262                    else {
263                            String stringFileContent = StringUtil.read(
264                                    urlConnection.getInputStream());
265    
266                            if (!StringUtil.endsWith(resourcePath, _CSS_MINIFIED_SUFFIX) &&
267                                    !StringUtil.endsWith(
268                                            resourcePath, _JAVASCRIPT_MINIFIED_SUFFIX)) {
269    
270                                    if (minifierType.equals("css")) {
271                                            try {
272                                                    stringFileContent = DynamicCSSUtil.parseSass(
273                                                            getServletContext(), request, resourcePath,
274                                                            stringFileContent);
275                                            }
276                                            catch (Exception e) {
277                                                    _log.error(
278                                                            "Unable to parse SASS on CSS " +
279                                                                    resourceURL.getPath(), e);
280    
281                                                    if (_log.isDebugEnabled()) {
282                                                            _log.debug(stringFileContent);
283                                                    }
284    
285                                                    response.setHeader(
286                                                            HttpHeaders.CACHE_CONTROL,
287                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
288                                            }
289    
290                                            String baseURL = StringPool.BLANK;
291    
292                                            int index = resourcePath.lastIndexOf(CharPool.SLASH);
293    
294                                            if (index != -1) {
295                                                    baseURL = resourcePath.substring(0, index + 1);
296                                            }
297    
298                                            stringFileContent = AggregateUtil.updateRelativeURLs(
299                                                    stringFileContent, baseURL);
300    
301                                            stringFileContent = MinifierUtil.minifyCss(
302                                                    stringFileContent);
303                                    }
304                                    else if (minifierType.equals("js")) {
305                                            stringFileContent = MinifierUtil.minifyJavaScript(
306                                                    stringFileContent);
307                                    }
308                            }
309    
310                            fileContentBag = new FileContentBag(
311                                    stringFileContent.getBytes(StringPool.UTF8),
312                                    urlConnection.getLastModified());
313                    }
314    
315                    if (PropsValues.COMBO_CHECK_TIMESTAMP) {
316                            int timeToLive =
317                                    (int)(PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL / Time.SECOND);
318    
319                            _fileContentBagPortalCache.put(
320                                    fileContentKey, fileContentBag, timeToLive);
321                    }
322    
323                    return fileContentBag._fileContent;
324            }
325    
326            protected URL getResourceURL(
327                            ServletContext servletContext, String rootPath, String path)
328                    throws Exception {
329    
330                    URL url = servletContext.getResource(path);
331    
332                    if (url == null) {
333                            return null;
334                    }
335    
336                    String filePath = ServletContextUtil.getResourcePath(url);
337    
338                    int pos = filePath.indexOf(
339                            rootPath.concat(StringPool.SLASH).concat(_JAVASCRIPT_DIR));
340    
341                    if (pos == 0) {
342                            return url;
343                    }
344    
345                    return null;
346            }
347    
348            protected boolean validateModuleExtension(String moduleName)
349                    throws Exception {
350    
351                    boolean validModuleExtension = false;
352    
353                    String[] fileExtensions = PrefsPropsUtil.getStringArray(
354                            PropsKeys.COMBO_ALLOWED_FILE_EXTENSIONS, StringPool.COMMA);
355    
356                    for (String fileExtension : fileExtensions) {
357                            if (StringPool.STAR.equals(fileExtension) ||
358                                    StringUtil.endsWith(moduleName, fileExtension)) {
359    
360                                    validModuleExtension = true;
361    
362                                    break;
363                            }
364                    }
365    
366                    return validModuleExtension;
367            }
368    
369            private static final String _CSS_EXTENSION = "css";
370    
371            private static final String _CSS_MINIFIED_SUFFIX = "-min.css";
372    
373            private static final FileContentBag _EMPTY_FILE_CONTENT_BAG =
374                    new FileContentBag(new byte[0], 0);
375    
376            private static final String _JAVASCRIPT_DIR = "html/js";
377    
378            private static final String _JAVASCRIPT_MINIFIED_SUFFIX = "-min.js";
379    
380            private static Log _log = LogFactoryUtil.getLog(ComboServlet.class);
381    
382            private PortalCache<String, byte[][]> _bytesArrayPortalCache =
383                    SingleVMPoolUtil.getCache(ComboServlet.class.getName());
384            private PortalCache<String, FileContentBag> _fileContentBagPortalCache =
385                    SingleVMPoolUtil.getCache(FileContentBag.class.getName());
386            private Set<String> _protectedParameters = SetUtil.fromArray(
387                    new String[] {"b", "browserId", "minifierType", "languageId", "t"});
388    
389            private static class FileContentBag implements Serializable {
390    
391                    public FileContentBag(byte[] fileContent, long lastModifiedTime) {
392                            _fileContent = fileContent;
393                            _lastModified = lastModifiedTime;
394                    }
395    
396                    private byte[] _fileContent;
397                    private long _lastModified;
398    
399            }
400    
401    }