View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.servlets;
20  
21  import java.io.IOException;
22  import java.util.ArrayList;
23  import java.util.List;
24  
25  import javax.servlet.RequestDispatcher;
26  import javax.servlet.ServletContext;
27  import javax.servlet.ServletException;
28  import javax.servlet.http.HttpServlet;
29  import javax.servlet.http.HttpServletRequest;
30  import javax.servlet.http.HttpServletResponse;
31  
32  import org.eclipse.jetty.util.URIUtil;
33  
34  /**
35   * <p>This servlet may be used to concatenate multiple resources into
36   * a single response.</p>
37   * <p>It is intended to be used to load multiple
38   * javascript or css files, but may be used for any content of the
39   * same mime type that can be meaningfully concatenated.</p>
40   * <p>The servlet uses {@link RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
41   * to combine the requested content, so dynamically generated content
42   * may be combined (Eg engine.js for DWR).</p>
43   * <p>The servlet uses parameter names of the query string as resource names
44   * relative to the context root.  So these script tags:</p>
45   * <pre>
46   * &lt;script type="text/javascript" src="../js/behaviour.js"&gt;&lt;/script&gt;
47   * &lt;script type="text/javascript" src="../js/ajax.js"&gt;&lt;/script&gt;
48   * &lt;script type="text/javascript" src="../chat/chat.js"&gt;&lt;/script&gt;
49   * </pre>
50   * <p>can be replaced with the single tag (with the {@code ConcatServlet}
51   * mapped to {@code /concat}):</p>
52   * <pre>
53   * &lt;script type="text/javascript" src="../concat?/js/behaviour.js&amp;/js/ajax.js&amp;/chat/chat.js"&gt;&lt;/script&gt;
54   * </pre>
55   * <p>The {@link ServletContext#getMimeType(String)} method is used to determine the
56   * mime type of each resource. If the types of all resources do not match, then a 415
57   * UNSUPPORTED_MEDIA_TYPE error is returned.</p>
58   * <p>If the init parameter {@code development} is set to {@code true} then the servlet
59   * will run in development mode and the content will be concatenated on every request.</p>
60   * <p>Otherwise the init time of the servlet is used as the lastModifiedTime of the combined content
61   * and If-Modified-Since requests are handled with 304 NOT Modified responses if
62   * appropriate. This means that when not in development mode, the servlet must be
63   * restarted before changed content will be served.</p>
64   */
65  public class ConcatServlet extends HttpServlet
66  {
67      private boolean _development;
68      private long _lastModified;
69  
70      @Override
71      public void init() throws ServletException
72      {
73          _lastModified = System.currentTimeMillis();
74          _development = Boolean.parseBoolean(getInitParameter("development"));
75      }
76  
77      /*
78       * @return The start time of the servlet unless in development mode, in which case -1 is returned.
79       */
80      @Override
81      protected long getLastModified(HttpServletRequest req)
82      {
83          return _development ? -1 : _lastModified;
84      }
85  
86      @Override
87      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
88      {
89          String query = request.getQueryString();
90          if (query == null)
91          {
92              response.sendError(HttpServletResponse.SC_NO_CONTENT);
93              return;
94          }
95  
96          List<RequestDispatcher> dispatchers = new ArrayList<>();
97          String[] parts = query.split("\\&");
98          String type = null;
99          for (String part : parts)
100         {
101             String path = URIUtil.canonicalPath(URIUtil.decodePath(part));
102             if (path == null)
103             {
104                 response.sendError(HttpServletResponse.SC_NOT_FOUND);
105                 return;
106             }
107 
108             // Verify that the path is not protected.
109             if (startsWith(path, "/WEB-INF/") || startsWith(path, "/META-INF/"))
110             {
111                 response.sendError(HttpServletResponse.SC_NOT_FOUND);
112                 return;
113             }
114 
115             String t = getServletContext().getMimeType(path);
116             if (t != null)
117             {
118                 if (type == null)
119                 {
120                     type = t;
121                 }
122                 else if (!type.equals(t))
123                 {
124                     response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
125                     return;
126                 }
127             }
128 
129             RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(path);
130             if (dispatcher != null)
131                 dispatchers.add(dispatcher);
132         }
133 
134         if (type != null)
135             response.setContentType(type);
136 
137         for (RequestDispatcher dispatcher : dispatchers)
138             dispatcher.include(request, response);
139     }
140 
141     private boolean startsWith(String path, String prefix)
142     {
143         // Case insensitive match.
144         return prefix.regionMatches(true, 0, path, 0, prefix.length());
145     }
146 }