"Split" ReaderException into subclasses to enable more useful error reporting
[zxing.git] / zxingorg / src / com / google / zxing / web / DecodeServlet.java
1 /*
2  * Copyright 2008 ZXing authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.google.zxing.web;
18
19 import com.google.zxing.BarcodeFormat;
20 import com.google.zxing.BinaryBitmap;
21 import com.google.zxing.ChecksumException;
22 import com.google.zxing.DecodeHintType;
23 import com.google.zxing.FormatException;
24 import com.google.zxing.LuminanceSource;
25 import com.google.zxing.MultiFormatReader;
26 import com.google.zxing.NotFoundException;
27 import com.google.zxing.Reader;
28 import com.google.zxing.ReaderException;
29 import com.google.zxing.Result;
30 import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
31 import com.google.zxing.client.result.ParsedResult;
32 import com.google.zxing.client.result.ResultParser;
33 import com.google.zxing.common.GlobalHistogramBinarizer;
34 import com.google.zxing.common.HybridBinarizer;
35
36 import org.apache.commons.fileupload.FileItem;
37 import org.apache.commons.fileupload.FileUploadException;
38 import org.apache.commons.fileupload.disk.DiskFileItemFactory;
39 import org.apache.commons.fileupload.servlet.ServletFileUpload;
40 import org.apache.commons.lang.StringEscapeUtils;
41 import org.apache.http.Header;
42 import org.apache.http.HttpMessage;
43 import org.apache.http.HttpResponse;
44 import org.apache.http.HttpVersion;
45 import org.apache.http.HttpEntity;
46 import org.apache.http.client.HttpClient;
47 import org.apache.http.client.methods.HttpGet;
48 import org.apache.http.client.methods.HttpUriRequest;
49 import org.apache.http.conn.scheme.PlainSocketFactory;
50 import org.apache.http.conn.scheme.Scheme;
51 import org.apache.http.conn.scheme.SchemeRegistry;
52 import org.apache.http.conn.ssl.SSLSocketFactory;
53 import org.apache.http.conn.ClientConnectionManager;
54 import org.apache.http.impl.client.DefaultHttpClient;
55 import org.apache.http.impl.conn.SingleClientConnManager;
56 import org.apache.http.params.BasicHttpParams;
57 import org.apache.http.params.HttpParams;
58 import org.apache.http.params.HttpProtocolParams;
59
60 import java.awt.image.BufferedImage;
61 import java.io.IOException;
62 import java.io.InputStream;
63 import java.io.OutputStreamWriter;
64 import java.io.Writer;
65 import java.net.SocketException;
66 import java.net.URI;
67 import java.net.URISyntaxException;
68 import java.net.UnknownHostException;
69 import java.util.Collection;
70 import java.util.Hashtable;
71 import java.util.List;
72 import java.util.Vector;
73 import java.util.logging.Logger;
74
75 import javax.imageio.ImageIO;
76 import javax.servlet.ServletConfig;
77 import javax.servlet.ServletException;
78 import javax.servlet.ServletRequest;
79 import javax.servlet.http.HttpServlet;
80 import javax.servlet.http.HttpServletRequest;
81 import javax.servlet.http.HttpServletResponse;
82
83 /**
84  * {@link HttpServlet} which decodes images containing barcodes. Given a URL, it will
85  * retrieve the image and decode it. It can also process image files uploaded via POST.
86  *
87  * @author Sean Owen
88  */
89 public final class DecodeServlet extends HttpServlet {
90
91   private static final long MAX_IMAGE_SIZE = 2000000L;
92
93   private static final Logger log = Logger.getLogger(DecodeServlet.class.getName());
94
95   static final Hashtable<DecodeHintType, Object> HINTS;
96
97   static {
98     HINTS = new Hashtable<DecodeHintType, Object>(5);
99     HINTS.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
100     Collection<BarcodeFormat> possibleFormats = new Vector<BarcodeFormat>();
101     possibleFormats.add(BarcodeFormat.UPC_A);
102     possibleFormats.add(BarcodeFormat.UPC_E);
103     possibleFormats.add(BarcodeFormat.EAN_8);
104     possibleFormats.add(BarcodeFormat.EAN_13);
105     possibleFormats.add(BarcodeFormat.CODE_39);
106     possibleFormats.add(BarcodeFormat.CODE_128);
107     possibleFormats.add(BarcodeFormat.ITF);
108     possibleFormats.add(BarcodeFormat.RSS14);    
109     possibleFormats.add(BarcodeFormat.QR_CODE);
110     possibleFormats.add(BarcodeFormat.DATAMATRIX);
111     possibleFormats.add(BarcodeFormat.PDF417);
112     HINTS.put(DecodeHintType.POSSIBLE_FORMATS, possibleFormats);
113   }
114
115   private HttpParams params;
116   private SchemeRegistry registry;
117   private DiskFileItemFactory diskFileItemFactory;
118
119   @Override
120   public void init(ServletConfig servletConfig) {
121
122     Logger logger = Logger.getLogger("com.google.zxing");
123     logger.addHandler(new ServletContextLogHandler(servletConfig.getServletContext()));
124
125     params = new BasicHttpParams();
126     HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
127
128     registry = new SchemeRegistry();
129     registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
130     registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
131
132     diskFileItemFactory = new DiskFileItemFactory();
133
134     log.info("DecodeServlet configured");
135   }
136
137   @Override
138   protected void doGet(HttpServletRequest request, HttpServletResponse response)
139       throws ServletException, IOException {
140     String imageURIString = request.getParameter("u");
141     if (imageURIString == null || imageURIString.length() == 0) {
142       response.sendRedirect("badurl.jspx");
143       return;
144     }
145
146     if (!(imageURIString.startsWith("http://") || imageURIString.startsWith("https://"))) {
147       imageURIString = "http://" + imageURIString;
148     }
149
150     URI imageURI;
151     try {
152       imageURI = new URI(imageURIString);
153     } catch (URISyntaxException urise) {
154       response.sendRedirect("badurl.jspx");
155       return;
156     }
157
158     ClientConnectionManager connectionManager = new SingleClientConnManager(params, registry);
159     HttpClient client = new DefaultHttpClient(connectionManager, params);
160
161     HttpUriRequest getRequest = new HttpGet(imageURI);
162     getRequest.addHeader("Connection", "close"); // Avoids CLOSE_WAIT socket issue?
163
164     try {
165
166       HttpResponse getResponse;
167       try {
168         getResponse = client.execute(getRequest);
169       } catch (IllegalArgumentException iae) {
170         // Thrown if hostname is bad or null
171         getRequest.abort();
172         response.sendRedirect("badurl.jspx");
173         return;
174       } catch (SocketException se) {
175         // Thrown if hostname is bad or null
176         getRequest.abort();
177         response.sendRedirect("badurl.jspx");
178         return;
179       } catch (UnknownHostException uhe) {
180         getRequest.abort();
181         response.sendRedirect("badurl.jspx");
182         return;
183       }
184
185       if (getResponse.getStatusLine().getStatusCode() != HttpServletResponse.SC_OK) {
186         response.sendRedirect("badurl.jspx");
187         return;
188       }
189       if (!isSizeOK(getResponse)) {
190         response.sendRedirect("badimage.jspx");
191         return;
192       }
193
194       log.info("Decoding " + imageURI);
195       HttpEntity entity = getResponse.getEntity();
196       InputStream is = entity.getContent();
197       try {
198         processStream(is, request, response);
199       } finally {
200         entity.consumeContent();
201         is.close();
202       }
203
204     } finally {
205       connectionManager.shutdown();
206     }
207
208   }
209
210   @Override
211   protected void doPost(HttpServletRequest request, HttpServletResponse response)
212           throws ServletException, IOException {
213
214     if (!ServletFileUpload.isMultipartContent(request)) {
215       response.sendRedirect("badimage.jspx");
216       return;
217     }
218
219     ServletFileUpload upload = new ServletFileUpload(diskFileItemFactory);
220     upload.setFileSizeMax(MAX_IMAGE_SIZE);
221
222     // Parse the request
223     try {
224       for (FileItem item : (List<FileItem>) upload.parseRequest(request)) {
225         if (!item.isFormField()) {
226           if (item.getSize() <= MAX_IMAGE_SIZE) {
227             log.info("Decoding uploaded file");
228             InputStream is = item.getInputStream();
229             try {
230               processStream(is, request, response);
231             } finally {
232               is.close();
233             }
234           } else {
235             response.sendRedirect("badimage.jspx");
236           }
237           break;
238         }
239       }
240     } catch (FileUploadException fue) {
241       response.sendRedirect("badimage.jspx");
242     }
243
244   }
245
246   private static void processStream(InputStream is, ServletRequest request,
247       HttpServletResponse response) throws ServletException, IOException {
248     BufferedImage image = ImageIO.read(is);
249     if (image == null) {
250       response.sendRedirect("badimage.jspx");
251       return;
252     }
253
254     Reader reader = new MultiFormatReader();
255     Result result;
256     try {
257       LuminanceSource source = new BufferedImageLuminanceSource(image);
258       BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
259       result = reader.decode(bitmap, HINTS);
260     } catch (ReaderException re) {
261       try {
262         // Try again with other binarizer
263         LuminanceSource source = new BufferedImageLuminanceSource(image);
264         BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
265         result = reader.decode(bitmap, HINTS);
266       } catch (NotFoundException nfe) {
267         log.info("Not found: " + re.toString());
268         response.sendRedirect("notfound.jspx");
269         return;
270       } catch (FormatException fe) {
271         log.info("Format problem: " + re.toString());
272         response.sendRedirect("format.jspx");
273         return;
274       } catch (ChecksumException ce) {
275         log.info("Checksum problem: " + re.toString());
276         response.sendRedirect("format.jspx");
277         return;
278       }
279     }
280
281     if (request.getParameter("full") == null) {
282       response.setContentType("text/plain");
283       response.setCharacterEncoding("UTF8");
284       Writer out = new OutputStreamWriter(response.getOutputStream(), "UTF8");
285       try {
286         out.write(result.getText());
287       } finally {
288         out.close();
289       }
290     } else {
291       request.setAttribute("result", result);
292       byte[] rawBytes = result.getRawBytes();
293       if (rawBytes != null) {
294         request.setAttribute("rawBytesString", arrayToString(rawBytes));
295       } else {
296         request.setAttribute("rawBytesString", "(Not applicable)");
297       }
298       String text = result.getText();
299       if (text != null) {
300         request.setAttribute("text", StringEscapeUtils.escapeXml(text));
301       } else {
302         request.setAttribute("text", "(Not applicable)");
303       }
304       ParsedResult parsedResult = ResultParser.parseResult(result);
305       request.setAttribute("parsedResult", parsedResult);
306       String displayResult = parsedResult.getDisplayResult();
307       if (displayResult != null) {
308         request.setAttribute("displayResult", StringEscapeUtils.escapeXml(displayResult));
309       } else {
310         request.setAttribute("displayResult", "(Not applicable)");
311       }
312       request.getRequestDispatcher("decoderesult.jspx").forward(request, response);
313     }
314   }
315
316   private static boolean isSizeOK(HttpMessage getResponse) {
317     Header lengthHeader = getResponse.getLastHeader("Content-Length");
318     if (lengthHeader != null) {
319       long length = Long.parseLong(lengthHeader.getValue());
320       if (length > MAX_IMAGE_SIZE) {
321         return false;
322       }
323     }
324     return true;
325   }
326
327   private static String arrayToString(byte[] bytes) {
328     int length = bytes.length;
329     StringBuilder result = new StringBuilder(length << 2);
330     int i = 0;
331     while (i < length) {
332       int max = Math.min(i + 8, length);
333       for (int j = i; j < max; j++) {
334         int value = bytes[j] & 0xFF;
335         result.append(Integer.toHexString(value / 16));
336         result.append(Integer.toHexString(value % 16));
337         result.append(' ');
338       }
339       result.append('\n');
340       i += 8;
341     }
342     for (int j = i - 8; j < length; j++) {
343       result.append(Integer.toHexString(bytes[j] & 0xFF));
344       result.append(' ');
345     }
346     return result.toString();
347   }
348
349   @Override
350   public void destroy() {
351     log.config("DecodeServlet shutting down...");
352   }
353
354 }