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.ArrayDeque;
23  import java.util.Queue;
24  import java.util.concurrent.ConcurrentHashMap;
25  import java.util.concurrent.ConcurrentMap;
26  
27  import javax.servlet.Filter;
28  import javax.servlet.FilterChain;
29  import javax.servlet.FilterConfig;
30  import javax.servlet.ServletException;
31  import javax.servlet.ServletRequest;
32  import javax.servlet.ServletRequestEvent;
33  import javax.servlet.ServletRequestListener;
34  import javax.servlet.ServletResponse;
35  import javax.servlet.http.HttpSession;
36  
37  import org.eclipse.jetty.http.HttpHeader;
38  import org.eclipse.jetty.http.HttpURI;
39  import org.eclipse.jetty.server.PushBuilder;
40  import org.eclipse.jetty.server.Request;
41  import org.eclipse.jetty.server.Response;
42  import org.eclipse.jetty.util.log.Log;
43  import org.eclipse.jetty.util.log.Logger;
44  
45  public class PushSessionCacheFilter implements Filter
46  {
47      private static final String TARGET_ATTR = "PushCacheFilter.target";
48      private static final String TIMESTAMP_ATTR = "PushCacheFilter.timestamp";
49      private static final Logger LOG = Log.getLogger(PushSessionCacheFilter.class);
50      private final ConcurrentMap<String, Target> _cache = new ConcurrentHashMap<>();
51      private long _associateDelay = 5000L;
52  
53      @Override
54      public void init(FilterConfig config) throws ServletException
55      {
56          if (config.getInitParameter("associateDelay") != null)
57              _associateDelay = Long.valueOf(config.getInitParameter("associateDelay"));
58  
59          // Add a listener that is used to collect information about associated resource,
60          // etags and modified dates
61          config.getServletContext().addListener(new ServletRequestListener()
62          {
63              // Collect information when request is destroyed.
64              @Override
65              public void requestDestroyed(ServletRequestEvent sre)
66              {
67                  Request request = Request.getBaseRequest(sre.getServletRequest());
68                  Target target = (Target)request.getAttribute(TARGET_ATTR);
69                  if (target == null)
70                      return;
71  
72                  // Update conditional data
73                  Response response = request.getResponse();
74                  target._etag = response.getHttpFields().get(HttpHeader.ETAG);
75                  target._lastModified = response.getHttpFields().get(HttpHeader.LAST_MODIFIED);
76  
77                  // Don't associate pushes
78                  if (request.isPush())
79                  {
80                      if (LOG.isDebugEnabled())
81                          LOG.debug("Pushed {} for {}", request.getResponse().getStatus(), request.getRequestURI());
82                      return;
83                  }
84                  else if (LOG.isDebugEnabled())
85                  {
86                      LOG.debug("Served {} for {}", request.getResponse().getStatus(), request.getRequestURI());
87                  }
88  
89                  // Does this request have a referer?
90                  String referer = request.getHttpFields().get(HttpHeader.REFERER);
91  
92                  if (referer != null)
93                  {
94                      // Is the referer from this contexts?
95                      HttpURI referer_uri = new HttpURI(referer);
96                      if (request.getServerName().equals(referer_uri.getHost()))
97                      {
98                          Target referer_target = _cache.get(referer_uri.getPath());
99                          if (referer_target != null)
100                         {
101                             HttpSession session = request.getSession();
102                             ConcurrentHashMap<String, Long> timestamps = (ConcurrentHashMap<String, Long>)session.getAttribute(TIMESTAMP_ATTR);
103                             Long last = timestamps.get(referer_target._path);
104                             if (last != null && (System.currentTimeMillis() - last) < _associateDelay)
105                             {
106                                 if (referer_target._associated.putIfAbsent(target._path, target) == null)
107                                 {
108                                     if (LOG.isDebugEnabled())
109                                         LOG.debug("ASSOCIATE {}->{}", referer_target._path, target._path);
110                                 }
111                             }
112                         }
113                     }
114                 }
115             }
116 
117             @Override
118             public void requestInitialized(ServletRequestEvent sre)
119             {
120             }
121         });
122     }
123 
124     @Override
125     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
126     {
127         // Get Jetty request as these APIs are not yet standard
128         Request baseRequest = Request.getBaseRequest(request);
129         String uri = baseRequest.getRequestURI();
130 
131         if (LOG.isDebugEnabled())
132             LOG.debug("{} {} push={}", baseRequest.getMethod(), uri, baseRequest.isPush());
133 
134         HttpSession session = baseRequest.getSession(true);
135 
136         // find the target for this resource
137         Target target = _cache.get(uri);
138         if (target == null)
139         {
140             Target t = new Target(uri);
141             target = _cache.putIfAbsent(uri, t);
142             target = target == null ? t : target;
143         }
144         request.setAttribute(TARGET_ATTR, target);
145 
146         // Set the timestamp for this resource in this session
147         ConcurrentHashMap<String, Long> timestamps = (ConcurrentHashMap<String, Long>)session.getAttribute(TIMESTAMP_ATTR);
148         if (timestamps == null)
149         {
150             timestamps = new ConcurrentHashMap<>();
151             session.setAttribute(TIMESTAMP_ATTR, timestamps);
152         }
153         timestamps.put(uri, System.currentTimeMillis());
154 
155         // push any associated resources
156         if (baseRequest.isPushSupported() && !baseRequest.isPush() && !target._associated.isEmpty())
157         {
158             // Breadth-first push of associated resources.
159             Queue<Target> queue = new ArrayDeque<>();
160             queue.offer(target);
161             while (!queue.isEmpty())
162             {
163                 Target parent = queue.poll();
164                 PushBuilder builder = baseRequest.getPushBuilder();
165                 builder.addHeader("X-Pusher", PushSessionCacheFilter.class.toString());
166                 for (Target child : parent._associated.values())
167                 {
168                     queue.offer(child);
169 
170                     String path = child._path;
171                     if (LOG.isDebugEnabled())
172                         LOG.debug("PUSH {} <- {}", path, uri);
173 
174                     builder.path(path).etag(child._etag).lastModified(child._lastModified).push();
175                 }
176             }
177         }
178 
179         chain.doFilter(request, response);
180     }
181 
182     @Override
183     public void destroy()
184     {
185         _cache.clear();
186     }
187 
188     private static class Target
189     {
190         private final String _path;
191         private final ConcurrentMap<String, Target> _associated = new ConcurrentHashMap<>();
192         private volatile String _etag;
193         private volatile String _lastModified;
194 
195         private Target(String path)
196         {
197             _path = path;
198         }
199 
200         @Override
201         public String toString()
202         {
203             return String.format("Target{p=%s,e=%s,m=%s,a=%d}", _path, _etag, _lastModified, _associated.size());
204         }
205     }
206 }