View Javadoc

1   package com.bradmcevoy.http.http11;
2   
3   import com.bradmcevoy.http.*;
4   import com.bradmcevoy.http.Response.Status;
5   import com.bradmcevoy.http.exceptions.BadRequestException;
6   import java.io.IOException;
7   import java.io.OutputStream;
8   import java.io.PrintWriter;
9   import java.util.Date;
10  import java.util.List;
11  import java.util.Map;
12  
13  import org.slf4j.Logger;
14  import org.slf4j.LoggerFactory;
15  
16  import com.bradmcevoy.http.exceptions.NotAuthorizedException;
17  import com.bradmcevoy.io.BufferingOutputStream;
18  import com.bradmcevoy.io.ReadingException;
19  import com.bradmcevoy.io.StreamUtils;
20  import com.bradmcevoy.io.WritingException;
21  import java.io.InputStream;
22  import org.apache.commons.io.IOUtils;
23  
24  /**
25   *
26   */
27  public class DefaultHttp11ResponseHandler implements Http11ResponseHandler {
28  
29      public enum BUFFERING {
30          always,
31          never,
32          whenNeeded
33      }
34  
35      private static final Logger log = LoggerFactory.getLogger( DefaultHttp11ResponseHandler.class );
36      public static final String METHOD_NOT_ALLOWED_HTML = "<html><body><h1>Method Not Allowed</h1></body></html>";
37      public static final String NOT_FOUND_HTML = "<html><body><h1>${url} Not Found (404)</h1></body></html>";
38      public static final String METHOD_NOT_IMPLEMENTED_HTML = "<html><body><h1>Method Not Implemented</h1></body></html>";
39      public static final String CONFLICT_HTML = "<html><body><h1>Conflict</h1></body></html>";
40      public static final String SERVER_ERROR_HTML = "<html><body><h1>Server Error</h1></body></html>";
41      public static final String NOT_AUTHORISED_HTML = "<html><body><h1>Not authorised</h1></body></html>";
42      private final AuthenticationService authenticationService;
43      private final ETagGenerator eTagGenerator;
44      private CacheControlHelper cacheControlHelper = new DefaultCacheControlHelper();
45      private int maxMemorySize = 100000;
46      private BUFFERING buffering;
47  
48      public DefaultHttp11ResponseHandler(AuthenticationService authenticationService) {
49          this.authenticationService = authenticationService;
50          this.eTagGenerator = new DefaultETagGenerator();
51      }
52  
53      public DefaultHttp11ResponseHandler(AuthenticationService authenticationService, ETagGenerator eTagGenerator) {
54          this.authenticationService = authenticationService;
55          this.eTagGenerator = eTagGenerator;
56      }
57  
58      /**
59       * Defaults to com.bradmcevoy.http.http11.DefaultCacheControlHelper
60       * @return
61       */
62      public CacheControlHelper getCacheControlHelper() {
63          return cacheControlHelper;
64      }
65  
66      public void setCacheControlHelper(CacheControlHelper cacheControlHelper) {
67          this.cacheControlHelper = cacheControlHelper;
68      }
69  
70      public String generateEtag(Resource r) {
71          return eTagGenerator.generateEtag(r);
72      }
73  
74      public void respondWithOptions(Resource resource, Response response, Request request, List<String> methodsAllowed) {
75          response.setStatus(Response.Status.SC_OK);
76          response.setAllowHeader(methodsAllowed);
77          response.setContentLengthHeader((long) 0);
78      }
79  
80      public void respondNotFound(Response response, Request request) {
81          response.setStatus(Response.Status.SC_NOT_FOUND);
82          response.setContentTypeHeader("text/html");
83          PrintWriter pw = new PrintWriter(response.getOutputStream(), true);
84  
85          String s = NOT_FOUND_HTML.replace("${url}", request.getAbsolutePath());
86          pw.print(s);
87          pw.flush();
88  
89      }
90  
91      public void respondUnauthorised(Resource resource, Response response, Request request) {
92          log.trace("respondUnauthorised");
93          response.setStatus(Response.Status.SC_UNAUTHORIZED);
94          List<String> challenges = authenticationService.getChallenges(resource, request);
95          response.setAuthenticateHeader(challenges);
96  
97  //        PrintWriter pw = new PrintWriter(response.getOutputStream(), true);
98  //
99  //        // http://jira.ettrema.com:8080/browse/MIL-39
100 //        String s = NOT_AUTHORISED_HTML.replace("${url}", request.getAbsolutePath());
101 //        response.setContentLengthHeader((long)s.length());
102 //        pw.print(s);
103 //        pw.flush();
104 
105     }
106 
107     public void respondMethodNotImplemented(Resource resource, Response response, Request request) {
108 //        log.debug( "method not implemented. resource: " + resource.getClass().getName() + " - method " + request.getMethod() );
109         try {
110             response.setStatus(Response.Status.SC_NOT_IMPLEMENTED);
111             OutputStream out = response.getOutputStream();
112             out.write(METHOD_NOT_IMPLEMENTED_HTML.getBytes());
113         } catch (IOException ex) {
114             log.warn("exception writing content");
115         }
116     }
117 
118     public void respondMethodNotAllowed(Resource res, Response response, Request request) {
119         log.debug("method not allowed. handler: " + this.getClass().getName() + " resource: " + res.getClass().getName());
120         try {
121             response.setStatus(Response.Status.SC_METHOD_NOT_ALLOWED);
122             OutputStream out = response.getOutputStream();
123             out.write(METHOD_NOT_ALLOWED_HTML.getBytes());
124         } catch (IOException ex) {
125             log.warn("exception writing content");
126         }
127     }
128 
129     /**
130      *
131      * @param resource
132      * @param response
133      * @param message - optional message to output in the body content
134      */
135     public void respondConflict(Resource resource, Response response, Request request, String message) {
136         log.debug("respondConflict");
137         try {
138             response.setStatus(Response.Status.SC_CONFLICT);
139             OutputStream out = response.getOutputStream();
140             out.write(CONFLICT_HTML.getBytes());
141         } catch (IOException ex) {
142             log.warn("exception writing content");
143         }
144     }
145 
146     public void respondRedirect(Response response, Request request, String redirectUrl) {
147         if (redirectUrl == null) {
148             throw new NullPointerException("redirectUrl cannot be null");
149         }
150         log.trace("respondRedirect");
151         // delegate to the response, because this can be server dependent
152         response.sendRedirect(redirectUrl);
153 //        response.setStatus(Response.Status.SC_MOVED_TEMPORARILY);
154 //        response.setLocationHeader(redirectUrl);
155     }
156 
157     public void respondExpectationFailed(Response response, Request request) {
158         response.setStatus(Response.Status.SC_EXPECTATION_FAILED);
159     }
160 
161     public void respondCreated(Resource resource, Response response, Request request) {
162 //        log.debug( "respondCreated" );
163         response.setStatus(Response.Status.SC_CREATED);
164     }
165 
166     public void respondNoContent(Resource resource, Response response, Request request) {
167 //        log.debug( "respondNoContent" );
168         //response.setStatus(Response.Status.SC_OK);
169         // see comments in http://www.ettrema.com:8080/browse/MIL-87
170         response.setStatus(Response.Status.SC_NO_CONTENT);
171     }
172 
173     public void respondPartialContent(GetableResource resource, Response response, Request request, Map<String, String> params, Range range) throws NotAuthorizedException, BadRequestException {
174         log.debug("respondPartialContent: " + range.getStart() + " - " + range.getFinish());
175         response.setStatus(Response.Status.SC_PARTIAL_CONTENT);
176         response.setContentRangeHeader(range.getStart(), range.getFinish(), resource.getContentLength());
177         response.setDateHeader(new Date());
178         String etag = eTagGenerator.generateEtag(resource);
179         if (etag != null) {
180             response.setEtag(etag);
181         }
182         String acc = request.getAcceptHeader();
183         String ct = resource.getContentType(acc);
184         if (ct != null) {
185             response.setContentTypeHeader(ct);
186         }
187         try {
188             resource.sendContent(response.getOutputStream(), range, params, ct);
189         } catch (IOException ex) {
190             log.warn("IOException writing to output, probably client terminated connection", ex);
191         }
192     }
193 
194     public void respondHead(Resource resource, Response response, Request request) {
195         setRespondContentCommonHeaders(response, resource, Response.Status.SC_NO_CONTENT, request.getAuthorization());
196     }
197 
198     public void respondContent(Resource resource, Response response, Request request, Map<String, String> params) throws NotAuthorizedException, BadRequestException {
199         log.debug("respondContent: " + resource.getClass());
200         Auth auth = request.getAuthorization();
201         setRespondContentCommonHeaders(response, resource, auth);
202         if (resource instanceof GetableResource) {
203             GetableResource gr = (GetableResource) resource;
204             String acc = request.getAcceptHeader();
205             String ct = gr.getContentType(acc);
206             if (ct != null) {
207                 ct = pickBestContentType(ct);
208                 response.setContentTypeHeader(ct);
209             }
210             cacheControlHelper.setCacheControl(gr, response, request.getAuthorization());
211 
212             Long contentLength = gr.getContentLength();
213             if (buffering == BUFFERING.always || (contentLength != null && buffering == BUFFERING.whenNeeded)) { // often won't know until rendered
214                 log.trace("sending content with known content length: " + contentLength);
215                 response.setContentLengthHeader(contentLength);
216                 sendContent(request, response, (GetableResource) resource, params, null, ct);
217             } else {
218                 log.trace("buffering content...");
219                 BufferingOutputStream tempOut = new BufferingOutputStream(maxMemorySize);
220                 try {
221                     ((GetableResource) resource).sendContent(tempOut, null, params, ct);
222                     tempOut.close();
223                 } catch (IOException ex) {
224                     tempOut.deleteTempFileIfExists();
225                     throw new RuntimeException("Exception generating buffered content", ex);
226                 }
227                 Long bufContentLength = tempOut.getSize();
228                 if (contentLength != null) {
229                     if (!contentLength.equals(bufContentLength)) {
230                         throw new RuntimeException("Lengthd dont match: " + contentLength + " != " + bufContentLength);
231                     }
232                 }
233                 log.trace("sending buffered content...");
234                 response.setContentLengthHeader(bufContentLength);
235                 InputStream in = tempOut.getInputStream();
236                 try {
237                     StreamUtils.readTo(in, response.getOutputStream());
238                 } catch (ReadingException ex) {
239                     throw new RuntimeException(ex);
240                 } catch (WritingException ex) {
241                     log.warn("exception writing, client probably closed connection", ex);
242                 } finally {
243                     IOUtils.closeQuietly(in); // make sure we close to delete temporary file
244                 }
245                 return;
246 
247 
248             }
249 
250         }
251     }
252 
253     public void respondNotModified(GetableResource resource, Response response, Request request) {
254         log.trace("respondNotModified");
255         response.setStatus(Response.Status.SC_NOT_MODIFIED);
256         response.setDateHeader(new Date());
257         String etag = eTagGenerator.generateEtag(resource);
258         if (etag != null) {
259             response.setEtag(etag);
260         }
261 
262         // Note that we use a simpler modified date handling here then when
263         // responding with content, because in a not-modified situation the
264         // modified date MUST be that of the actual resource
265         Date modDate = resource.getModifiedDate();
266         response.setLastModifiedHeader(modDate);
267 
268         cacheControlHelper.setCacheControl(resource, response, request.getAuthorization());
269     }
270 
271     protected void sendContent(Request request, Response response, GetableResource resource, Map<String, String> params, Range range, String contentType) throws NotAuthorizedException, BadRequestException {
272         long l = System.currentTimeMillis();
273         log.trace("sendContent");
274         OutputStream out = outputStreamForResponse(request, response, resource);
275         try {
276             resource.sendContent(out, null, params, contentType);
277             out.flush();
278             if (log.isTraceEnabled()) {
279                 l = System.currentTimeMillis() - l;
280                 log.trace("sendContent finished in " + l + "ms");
281             }
282         } catch (IOException ex) {
283             log.warn("IOException sending content", ex);
284         }
285     }
286 
287     protected OutputStream outputStreamForResponse(Request request, Response response, GetableResource resource) {
288         OutputStream outToUse = response.getOutputStream();
289         return outToUse;
290     }
291 
292     protected void output(final Response response, final String s) {
293         PrintWriter pw = new PrintWriter(response.getOutputStream(), true);
294         pw.print(s);
295         pw.flush();
296     }
297 
298     protected void setRespondContentCommonHeaders(Response response, Resource resource, Auth auth) {
299         setRespondContentCommonHeaders(response, resource, Response.Status.SC_OK, auth);
300     }
301 
302     protected void setRespondContentCommonHeaders(Response response, Resource resource, Response.Status status, Auth auth) {
303         response.setStatus(status);
304         response.setDateHeader(new Date());
305         String etag = eTagGenerator.generateEtag(resource);
306         if (etag != null) {
307             response.setEtag(etag);
308         }
309         setModifiedDate(response, resource, auth);
310     }
311 
312     /**
313     The modified date response header is used by the client for content
314     caching. It seems obvious that if we have a modified date on the resource
315     we should set it.
316     BUT, because of the interaction with max-age we should always set it
317     to the current date if we have max-age
318     The problem, is that if we find that a condition GET has an expired mod-date
319     (based on maxAge) then we want to respond with content (even if our mod-date
320     hasnt changed. But if we use the actual mod-date in that case, then the
321     browser will continue to use the old mod-date, so will forever more respond
322     with content. So we send a mod-date of now to ensure that future requests
323     will be given a 304 not modified.*
324      *
325      * @param response
326      * @param resource
327      * @param auth
328      */
329     public static void setModifiedDate(Response response, Resource resource, Auth auth) {
330         Date modDate = resource.getModifiedDate();
331         if (modDate != null) {
332             // HACH - see if this helps IE
333             response.setLastModifiedHeader(modDate);
334 //            if (resource instanceof GetableResource) {
335 //                GetableResource gr = (GetableResource) resource;
336 //                Long maxAge = gr.getMaxAgeSeconds(auth);
337 //                if (maxAge != null && maxAge > 0) {
338 //                    log.trace("setModifiedDate: has a modified date and a positive maxAge, so adjust modDate");
339 //                    long tm = System.currentTimeMillis() - 60000; // modified 1 minute ago
340 //                    modDate = new Date(tm); // have max-age, so use current date
341 //                }
342 //            }
343 //            response.setLastModifiedHeader(modDate);
344         }
345     }
346 
347     public void respondBadRequest(Resource resource, Response response, Request request) {
348         response.setStatus(Response.Status.SC_BAD_REQUEST);
349     }
350 
351     public void respondForbidden(Resource resource, Response response, Request request) {
352         response.setStatus(Response.Status.SC_FORBIDDEN);
353     }
354 
355     public void respondDeleteFailed(Request request, Response response, Resource resource, Status status) {
356         response.setStatus(status);
357     }
358 
359     public AuthenticationService getAuthenticationService() {
360         return authenticationService;
361     }
362 
363     public void respondServerError(Request request, Response response, String reason) {
364         try {
365             response.setStatus(Status.SC_INTERNAL_SERVER_ERROR);
366             OutputStream out = response.getOutputStream();
367             out.write(SERVER_ERROR_HTML.getBytes());
368         } catch (IOException ex) {
369             throw new RuntimeException(ex);
370         }
371     }
372 
373     /**
374      * Maximum size of data to hold in memory per request when buffering output
375      * data.
376      *
377      * @return
378      */
379     public int getMaxMemorySize() {
380         return maxMemorySize;
381     }
382 
383     public void setMaxMemorySize(int maxMemorySize) {
384         this.maxMemorySize = maxMemorySize;
385     }
386 
387     public BUFFERING getBuffering() {
388         return buffering;
389     }
390 
391     public void setBuffering(BUFFERING buffering) {
392         this.buffering = buffering;
393     }
394 
395     /**
396      * Sometimes we'll get a content type list, such as image/jpeg,image/pjpeg
397      *
398      * In this case we should pick the first in the list
399      *
400      * @param ct
401      * @return
402      */
403     private String pickBestContentType(String ct) {
404         if( ct == null ) {
405             return null;
406         } else if( ct.contains(",")) {
407             return ct.split(",")[0];
408         } else {
409             return ct;
410         }
411     }
412 
413 }