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