View Javadoc
1   /*
2    * Copyright (C) 2013 4th Line GmbH, Switzerland
3    *
4    * The contents of this file are subject to the terms of either the GNU
5    * Lesser General Public License Version 2 or later ("LGPL") or the
6    * Common Development and Distribution License Version 1 or later
7    * ("CDDL") (collectively, the "License"). You may not use this file
8    * except in compliance with the License. See LICENSE.txt for more
9    * information.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14   */
15  
16  package org.fourthline.cling.transport.impl;
17  
18  import org.fourthline.cling.model.message.Connection;
19  import org.fourthline.cling.model.message.StreamRequestMessage;
20  import org.fourthline.cling.model.message.StreamResponseMessage;
21  import org.fourthline.cling.model.message.UpnpHeaders;
22  import org.fourthline.cling.model.message.UpnpMessage;
23  import org.fourthline.cling.model.message.UpnpRequest;
24  import org.fourthline.cling.protocol.ProtocolFactory;
25  import org.fourthline.cling.transport.spi.UpnpStream;
26  import org.seamless.util.Exceptions;
27  import org.seamless.util.io.IO;
28  
29  import javax.servlet.AsyncContext;
30  import javax.servlet.AsyncEvent;
31  import javax.servlet.AsyncListener;
32  import javax.servlet.ServletResponse;
33  import javax.servlet.http.HttpServletRequest;
34  import javax.servlet.http.HttpServletResponse;
35  import java.io.IOException;
36  import java.io.InputStream;
37  import java.net.HttpURLConnection;
38  import java.net.URI;
39  import java.util.Enumeration;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.logging.Level;
43  import java.util.logging.Logger;
44  
45  /**
46   * Implementation based on Servlet 3.0 API.
47   * <p>
48   * Concrete implementations must provide a connection wrapper, as this wrapper most likely has
49   * to access proprietary APIs to implement connection checking.
50   * </p>
51   *
52   * @author Christian Bauer
53   */
54  public abstract class AsyncServletUpnpStream extends UpnpStream implements AsyncListener {
55  
56      final private static Logger log = Logger.getLogger(UpnpStream.class.getName());
57  
58      final protected AsyncContext asyncContext;
59      final protected HttpServletRequest request;
60  
61      protected StreamResponseMessage responseMessage;
62  
63      public AsyncServletUpnpStream(ProtocolFactory protocolFactory,
64                                    AsyncContext asyncContext,
65                                    HttpServletRequest request) {
66          super(protocolFactory);
67          this.asyncContext = asyncContext;
68          this.request = request;
69          asyncContext.addListener(this);
70      }
71  
72      protected HttpServletRequest getRequest() {
73          return request;
74      }
75  
76      protected HttpServletResponse getResponse() {
77          ServletResponse response;
78          if ((response = asyncContext.getResponse()) == null) {
79              throw new IllegalStateException(
80                  "Couldn't get response from asynchronous context, already timed out"
81              );
82          }
83          return (HttpServletResponse) response;
84      }
85  
86      protected void complete() {
87          try {
88              asyncContext.complete();
89          } catch (IllegalStateException ex) {
90              // If Jetty's connection, for whatever reason, is in an illegal state, this will be thrown
91              // and we can "probably" ignore it. The request is complete, no matter how it ended.
92              log.info("Error calling servlet container's AsyncContext#complete() method: " + ex);
93          }
94      }
95  
96      @Override
97      public void run() {
98          try {
99              StreamRequestMessage requestMessage = readRequestMessage();
100             if (log.isLoggable(Level.FINER))
101                 log.finer("Processing new request message: " + requestMessage);
102 
103             responseMessage = process(requestMessage);
104 
105             if (responseMessage != null) {
106                 if (log.isLoggable(Level.FINER))
107                     log.finer("Preparing HTTP response message: " + responseMessage);
108                 writeResponseMessage(responseMessage);
109             } else {
110                 // If it's null, it's 404
111                 if (log.isLoggable(Level.FINER))
112                     log.finer("Sending HTTP response status: " + HttpURLConnection.HTTP_NOT_FOUND);
113                 getResponse().setStatus(HttpServletResponse.SC_NOT_FOUND);
114             }
115 
116         } catch (Throwable t) {
117             log.info("Exception occurred during UPnP stream processing: " + t);
118             if (log.isLoggable(Level.FINER)) {
119                 log.log(Level.FINER, "Cause: " + Exceptions.unwrap(t), Exceptions.unwrap(t));
120             }
121             if (!getResponse().isCommitted()) {
122                 log.finer("Response hasn't been committed, returning INTERNAL SERVER ERROR to client");
123                 getResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
124             } else {
125                 log.info("Could not return INTERNAL SERVER ERROR to client, response was already committed");
126             }
127             responseException(t);
128         } finally {
129             complete();
130         }
131     }
132 
133     @Override
134     public void onStartAsync(AsyncEvent event) throws IOException {
135         // This is a completely useless callback, it will only be called on request.startAsync() which
136         // then immediately removes the listener... what were they thinking.
137     }
138 
139     @Override
140     public void onComplete(AsyncEvent event) throws IOException {
141         if (log.isLoggable(Level.FINER))
142             log.finer("Completed asynchronous processing of HTTP request: " + event.getSuppliedRequest());
143         responseSent(responseMessage);
144     }
145 
146     @Override
147     public void onTimeout(AsyncEvent event) throws IOException {
148         if (log.isLoggable(Level.FINER))
149             log.finer("Asynchronous processing of HTTP request timed out: " + event.getSuppliedRequest());
150         responseException(new Exception("Asynchronous request timed out"));
151     }
152 
153     @Override
154     public void onError(AsyncEvent event) throws IOException {
155         if (log.isLoggable(Level.FINER))
156             log.finer("Asynchronous processing of HTTP request error: " + event.getThrowable());
157         responseException(event.getThrowable());
158     }
159 
160     protected StreamRequestMessage readRequestMessage() throws IOException {
161         // Extract what we need from the HTTP httpRequest
162         String requestMethod = getRequest().getMethod();
163         String requestURI = getRequest().getRequestURI();
164 
165         if (log.isLoggable(Level.FINER))
166             log.finer("Processing HTTP request: " + requestMethod + " " + requestURI);
167 
168         StreamRequestMessage requestMessage;
169         try {
170             requestMessage =
171                 new StreamRequestMessage(
172                     UpnpRequest.Method.getByHttpName(requestMethod),
173                     URI.create(requestURI)
174                 );
175         } catch (IllegalArgumentException ex) {
176             throw new RuntimeException("Invalid request URI: " + requestURI, ex);
177         }
178 
179         if (requestMessage.getOperation().getMethod().equals(UpnpRequest.Method.UNKNOWN)) {
180             throw new RuntimeException("Method not supported: " + requestMethod);
181         }
182 
183         // Connection wrapper
184         requestMessage.setConnection(createConnection());
185 
186         // Headers
187         UpnpHeaders headers = new UpnpHeaders();
188         Enumeration<String> headerNames = getRequest().getHeaderNames();
189         while (headerNames.hasMoreElements()) {
190             String headerName = headerNames.nextElement();
191             Enumeration<String> headerValues = getRequest().getHeaders(headerName);
192             while (headerValues.hasMoreElements()) {
193                 String headerValue = headerValues.nextElement();
194                 headers.add(headerName, headerValue);
195             }
196         }
197         requestMessage.setHeaders(headers);
198 
199         // Body
200         byte[] bodyBytes;
201         InputStream is = null;
202         try {
203             is = getRequest().getInputStream();
204             bodyBytes = IO.readBytes(is);
205         } finally {
206             if (is != null)
207                 is.close();
208         }
209         if (log.isLoggable(Level.FINER))
210             log.finer("Reading request body bytes: " + bodyBytes.length);
211 
212         if (bodyBytes.length > 0 && requestMessage.isContentTypeMissingOrText()) {
213 
214             if (log.isLoggable(Level.FINER))
215                 log.finer("Request contains textual entity body, converting then setting string on message");
216             requestMessage.setBodyCharacters(bodyBytes);
217 
218         } else if (bodyBytes.length > 0) {
219 
220             if (log.isLoggable(Level.FINER))
221                 log.finer("Request contains binary entity body, setting bytes on message");
222             requestMessage.setBody(UpnpMessage.BodyType.BYTES, bodyBytes);
223 
224         } else {
225             if (log.isLoggable(Level.FINER))
226                 log.finer("Request did not contain entity body");
227         }
228 
229         return requestMessage;
230     }
231 
232     protected void writeResponseMessage(StreamResponseMessage responseMessage) throws IOException {
233         if (log.isLoggable(Level.FINER))
234             log.finer("Sending HTTP response status: " + responseMessage.getOperation().getStatusCode());
235 
236         getResponse().setStatus(responseMessage.getOperation().getStatusCode());
237 
238         // Headers
239         for (Map.Entry<String, List<String>> entry : responseMessage.getHeaders().entrySet()) {
240             for (String value : entry.getValue()) {
241                 getResponse().addHeader(entry.getKey(), value);
242             }
243         }
244         // The Date header is recommended in UDA
245         getResponse().setDateHeader("Date", System.currentTimeMillis());
246 
247         // Body
248         byte[] responseBodyBytes = responseMessage.hasBody() ? responseMessage.getBodyBytes() : null;
249         int contentLength = responseBodyBytes != null ? responseBodyBytes.length : -1;
250 
251         if (contentLength > 0) {
252             getResponse().setContentLength(contentLength);
253             log.finer("Response message has body, writing bytes to stream...");
254             IO.writeBytes(getResponse().getOutputStream(), responseBodyBytes);
255         }
256     }
257 
258     abstract protected Connection createConnection();
259 
260 }