1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 package nonblocking;
18 
19 import java.io.IOException;
20 import java.nio.charset.StandardCharsets;
21 
22 import javax.servlet.AsyncContext;
23 import javax.servlet.ReadListener;
24 import javax.servlet.ServletException;
25 import javax.servlet.ServletInputStream;
26 import javax.servlet.ServletOutputStream;
27 import javax.servlet.WriteListener;
28 import javax.servlet.http.HttpServlet;
29 import javax.servlet.http.HttpServletRequest;
30 import javax.servlet.http.HttpServletResponse;
31 
32 /**
33  * This doesn't do anything particularly useful - it just counts the total
34  * number of bytes in a request body while demonstrating how to perform
35  * non-blocking reads.
36  */
37 public class ByteCounter extends HttpServlet {
38 
39     private static final long serialVersionUID = 1L;
40 
41     @Override
doGet(HttpServletRequest req, HttpServletResponse resp)42     protected void doGet(HttpServletRequest req, HttpServletResponse resp)
43             throws ServletException, IOException {
44 
45         resp.setContentType("text/plain");
46         resp.setCharacterEncoding("UTF-8");
47 
48         resp.getWriter().println("Try again using a POST request.");
49     }
50 
51     @Override
doPost(HttpServletRequest req, HttpServletResponse resp)52     protected void doPost(HttpServletRequest req, HttpServletResponse resp)
53             throws ServletException, IOException {
54 
55         resp.setContentType("text/plain");
56         resp.setCharacterEncoding("UTF-8");
57 
58         // Non-blocking IO requires async
59         AsyncContext ac = req.startAsync();
60 
61         // Use a single listener for read and write. Listeners often need to
62         // share state to coordinate reads and writes and this is much easier as
63         // a single object.
64         @SuppressWarnings("unused")
65         CounterListener listener = new CounterListener(
66                 ac, req.getInputStream(), resp.getOutputStream());
67     }
68 
69 
70     /**
71      * Keep in mind that each call may well be on a different thread to the
72      * previous call. Ensure that changes in values will be visible across
73      * threads. There should only ever be one container thread at a time calling
74      * the listener.
75      */
76     private static class CounterListener implements ReadListener, WriteListener {
77 
78         private final AsyncContext ac;
79         private final ServletInputStream sis;
80         private final ServletOutputStream sos;
81 
82         private volatile boolean readFinished = false;
83         private volatile long totalBytesRead = 0;
84         private byte[] buffer = new byte[8192];
85 
CounterListener(AsyncContext ac, ServletInputStream sis, ServletOutputStream sos)86         private CounterListener(AsyncContext ac, ServletInputStream sis,
87                 ServletOutputStream sos) {
88             this.ac = ac;
89             this.sis = sis;
90             this.sos = sos;
91 
92             // In Tomcat, the order the listeners are set controls the order
93             // that the first calls are made. In this case, the read listener
94             // will be called before the write listener.
95             sis.setReadListener(this);
96             sos.setWriteListener(this);
97         }
98 
99         @Override
onDataAvailable()100         public void onDataAvailable() throws IOException {
101             int read = 0;
102             // Loop as long as there is data to read. If isReady() returns false
103             // the socket will be added to the poller and onDataAvailable() will
104             // be called again as soon as there is more data to read.
105             while (sis.isReady() && read > -1) {
106                 read = sis.read(buffer);
107                 if (read > 0) {
108                     totalBytesRead += read;
109                 }
110             }
111         }
112 
113         @Override
onAllDataRead()114         public void onAllDataRead() throws IOException {
115             readFinished = true;
116 
117             // If sos is not ready to write data, the call to isReady() will
118             // register the socket with the poller which will trigger a call to
119             // onWritePossible() when the socket is ready to have data written
120             // to it.
121             if (sos.isReady()) {
122                 onWritePossible();
123             }
124         }
125 
126         @Override
onWritePossible()127         public void onWritePossible() throws IOException {
128             if (readFinished) {
129                 // Must be ready to write data if onWritePossible was called
130                 String msg = "Total bytes written = [" + totalBytesRead + "]";
131                 sos.write(msg.getBytes(StandardCharsets.UTF_8));
132                 ac.complete();
133             }
134         }
135 
136         @Override
onError(Throwable throwable)137         public void onError(Throwable throwable) {
138             // Should probably log the throwable
139             ac.complete();
140         }
141     }
142 }
143