Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / admin / AbstractServerConnection.java
blob9eb8ee4534fa9cb707ac3e47f59980eb2ae0e481
1 // Copyright 2009 Google Inc. All rights reserved.
3 package com.google.appengine.tools.admin;
5 import com.google.appengine.tools.admin.AppAdminFactory.ConnectOptions;
7 import java.io.BufferedReader;
8 import java.io.File;
9 import java.io.FileInputStream;
10 import java.io.IOException;
11 import java.io.InputStream;
12 import java.io.InputStreamReader;
13 import java.io.OutputStream;
14 import java.io.OutputStreamWriter;
15 import java.io.PrintWriter;
16 import java.io.StringReader;
17 import java.io.UnsupportedEncodingException;
18 import java.net.HttpURLConnection;
19 import java.net.MalformedURLException;
20 import java.net.URL;
21 import java.net.URLEncoder;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.logging.Logger;
27 /**
28 * Connection to the AppEngine hosting service, as set by {@link ConnectOptions}
31 public abstract class AbstractServerConnection implements ServerConnection {
33 private static final int MAX_SEND_RETRIES = 3;
35 protected interface DataPoster {
36 void post(OutputStream s) throws IOException;
39 private static class FilePoster implements DataPoster {
40 private static final int BUFFER_SIZE = 4 * 1024;
41 private File file;
43 public FilePoster(File file) {
44 assert (file != null && file.exists());
45 this.file = file;
48 @Override
49 public void post(OutputStream out) throws IOException {
50 InputStream in = new FileInputStream(file);
51 try {
52 byte[] buf = new byte[BUFFER_SIZE];
53 int len;
54 while ((len = in.read(buf)) != -1) {
55 out.write(buf, 0, len);
57 } finally {
58 in.close();
63 static class StringPoster implements DataPoster {
64 private String str;
66 public StringPoster(String s) {
67 str = s;
70 @Override
71 public void post(OutputStream s) throws IOException {
72 s.write(str.getBytes("UTF-8"));
76 protected static final String POST = "POST";
78 protected static final String GET = "GET";
80 protected ConnectOptions options;
82 protected static Logger logger =
83 Logger.getLogger(AbstractServerConnection.class.getCanonicalName());
85 protected AbstractServerConnection() {
88 protected AbstractServerConnection(ConnectOptions options) {
89 this.options = options;
90 if (System.getProperty("http.proxyHost") != null) {
91 logger.info("proxying HTTP through " + System.getProperty("http.proxyHost") + ":"
92 + System.getProperty("http.proxyPort"));
94 if (System.getProperty("https.proxyHost") != null) {
95 logger.info("proxying HTTPS through " + System.getProperty("https.proxyHost") + ":"
96 + System.getProperty("https.proxyPort"));
100 protected String buildQuery(Map<String, String> params) throws UnsupportedEncodingException {
101 StringBuffer buf = new StringBuffer();
102 for (String key : params.keySet()) {
103 buf.append(URLEncoder.encode(key, "UTF-8"));
104 buf.append('=');
105 buf.append(URLEncoder.encode(params.get(key), "UTF-8"));
106 buf.append('&');
108 return buf.toString();
111 protected URL buildURL(String path) throws MalformedURLException {
112 String protocol = options.getSecure() ? "https" : "http";
113 return new URL(protocol + "://" + options.getServer() + path);
116 protected IOException connect(String method, HttpURLConnection conn, DataPoster data)
117 throws IOException {
118 doPreConnect(method, conn, data);
119 conn.setInstanceFollowRedirects(false);
120 conn.setRequestMethod(method);
122 if (POST.equals(method)) {
123 conn.setDoOutput(true);
124 OutputStream out = conn.getOutputStream();
125 if (data != null) {
126 data.post(out);
128 out.close();
131 try {
132 conn.getInputStream();
133 } catch (IOException ex) {
134 return ex;
135 } finally {
136 doPostConnect(method, conn, data);
139 return null;
142 protected String constructHttpErrorMessage(HttpURLConnection conn, BufferedReader reader)
143 throws IOException {
144 StringBuilder sb = new StringBuilder("Error posting to URL: ");
145 sb.append(conn.getURL());
146 sb.append('\n');
147 sb.append(conn.getResponseCode());
148 sb.append(' ');
149 sb.append(conn.getResponseMessage());
150 sb.append('\n');
151 if (reader != null) {
152 for (String line; (line = reader.readLine()) != null;) {
153 sb.append(line);
154 sb.append('\n');
157 return sb.toString();
160 protected abstract void doHandleSendErrors(int status, URL url, HttpURLConnection conn,
161 BufferedReader connReader) throws IOException;
163 protected abstract void doPostConnect(String method, HttpURLConnection conn, DataPoster data)
164 throws IOException;
166 protected abstract void doPreConnect(String method, HttpURLConnection conn, DataPoster data)
167 throws IOException;
169 @Override
170 public String get(String url, Map<String, String> params) throws IOException {
171 return send(GET, url, null, null, params);
174 private static InputStream getInputStream(HttpURLConnection conn) {
175 InputStream is;
176 try {
177 return conn.getInputStream();
178 } catch (IOException ex) {
179 return conn.getErrorStream();
183 private static BufferedReader getReader(InputStream is) {
184 if (is == null) {
185 return null;
187 return new BufferedReader(new InputStreamReader(is));
190 protected static String getString(InputStream is) throws IOException {
191 StringBuffer response = new StringBuffer();
192 InputStreamReader reader = new InputStreamReader(is);
193 char buffer[] = new char[8192];
194 int read = 0;
195 while ((read = reader.read(buffer)) != -1) {
196 response.append(buffer, 0, read);
198 return response.toString();
201 protected static BufferedReader getReader(HttpURLConnection conn) {
202 return getReader(getInputStream(conn));
205 @Override
206 public String post(String url, File payload, String contentType, Map<String, String> params)
207 throws IOException {
208 return send(POST, url, new FilePoster(payload), contentType, params);
211 protected InputStream postAndGetInputStream(String url, DataPoster poster, String... params)
212 throws IOException {
213 return send1(POST, url, poster, null, getParamMap(params));
216 @Override
217 public String post(String url, File payload, String contentType, String... params)
218 throws IOException {
219 return send(POST, url, new FilePoster(payload), contentType, getParamMap(params));
222 @Override
223 public String post(String url, List<AppVersionUpload.FileInfo> payload, String... params)
224 throws IOException {
225 return sendBatch(POST, url, payload, getParamMap(params));
228 @Override
229 public String post(String url, String payload, Map<String, String> params) throws IOException {
230 return send(POST, url, new StringPoster(payload), null, params);
233 @Override
234 public String post(String url, String payload, String... params) throws IOException {
235 return send(POST, url, new StringPoster(payload), null, getParamMap(params));
239 * Returns an input stream that reads from this open connection. If the server returned
240 * an error (404, etc), return the error stream instead.
242 * @return {@code InputStream} for the server response.
244 @Override
245 public InputStream postAndGetInputStream(String url, String payload, String... params)
246 throws IOException {
247 return send1(POST, url, new StringPoster(payload), null, getParamMap(params));
250 protected HttpURLConnection openConnection(URL url) throws IOException {
251 return (HttpURLConnection) url.openConnection();
254 protected String send(String method, String path, DataPoster payload, String content_type,
255 Map<String, String> params) throws IOException {
256 return getString(send1(method, path, payload, content_type, params));
259 private Map<String, String> getParamMap(String... params) {
260 Map<String, String> paramMap = new HashMap<String, String>();
261 for (int i = 0; i < params.length; i += 2) {
262 paramMap.put(params[i], params[i + 1]);
264 return paramMap;
267 private InputStream send1(String method, String path, DataPoster payload, String content_type,
268 Map<String, String> params) throws IOException {
270 URL url = buildURL(path + '?' + buildQuery(params));
272 if (content_type == null) {
273 content_type = Application.guessContentTypeFromName(path);
276 int tries = 0;
277 while (true) {
278 HttpURLConnection conn = openConnection(url);
279 conn.setRequestProperty("Content-type", content_type);
280 conn.setRequestProperty("X-appcfg-api-version", "1");
282 if (options.getHost() != null) {
283 conn.setRequestProperty("Host", options.getHost());
286 IOException ioe = connect(method, conn, payload);
288 int status = conn.getResponseCode();
290 if (status == HttpURLConnection.HTTP_OK) {
291 return getInputStream(conn);
292 } else {
293 BufferedReader reader = getReader(conn);
294 StringBuilder sb = new StringBuilder();
295 for (String line; (line = reader.readLine()) != null;) {
296 sb.append(line);
298 String response = sb.toString();
299 String httpErrorMessage = constructHttpErrorMessage(
300 conn,
301 new BufferedReader(new StringReader(response)));
302 logger.warning(httpErrorMessage + "This is try #" + tries);
303 if (++tries > MAX_SEND_RETRIES) {
304 throw new HttpIoException(httpErrorMessage, status);
306 doHandleSendErrors(status, url, conn, new BufferedReader(new StringReader(response)));
311 protected String sendBatch(String method, String path, List<AppVersionUpload.FileInfo> payload,
312 Map<String, String> params) throws IOException {
313 return getString(sendBatchPayload(method, path, payload, params));
316 private InputStream sendBatchPayload(String method, String path,
317 List<AppVersionUpload.FileInfo> payload,
318 Map<String, String> params) throws IOException {
320 URL url = buildURL(path + '?' + buildQuery(params));
322 int tries = 0;
323 while (true) {
324 String boundary = "boundary" + Long
325 .toHexString(System.currentTimeMillis());
326 HttpURLConnection conn = openConnection(url);
327 conn.setRequestProperty("MIME-Version", "1.0");
328 conn.setRequestProperty("Content-Type", "message/rfc822");
329 conn.setRequestProperty("X-appcfg-api-version", "1");
330 doPreConnect(method, conn, null);
331 conn.setDoOutput(true);
332 conn.setRequestMethod(method);
334 if (options.getHost() != null) {
335 conn.setRequestProperty("Host", options.getHost());
337 conn.setInstanceFollowRedirects(false);
339 populateMixedPayloadStream(conn.getOutputStream(), payload, boundary);
341 doPostConnect(method, conn, null);
343 int status = conn.getResponseCode();
344 if (status == HttpURLConnection.HTTP_OK) {
345 return getInputStream(conn);
346 } else {
347 BufferedReader reader = getReader(conn);
348 StringBuilder sb = new StringBuilder();
349 for (String line; (line = reader.readLine()) != null;) {
350 sb.append(line);
352 String response = sb.toString();
353 String httpErrorMessage = constructHttpErrorMessage(
354 conn,
355 new BufferedReader(new StringReader(response)));
356 logger.warning(httpErrorMessage + "This is try #" + tries);
357 if (++tries > MAX_SEND_RETRIES) {
358 throw new IOException(httpErrorMessage);
360 doHandleSendErrors(status, url, conn, new BufferedReader(new StringReader(response)));
366 void populateMixedPayloadStream(OutputStream output,
367 List<AppVersionUpload.FileInfo> payload, String boundary) throws IOException {
369 String charset = "UTF-8";
370 String lf = "\n";
371 PrintWriter writer = null;
372 try {
373 writer = new PrintWriter(new OutputStreamWriter(output, charset),
374 true);
375 writer.append("Content-Type: multipart/mixed; boundary=\"" + boundary + "\"").append(lf);
376 writer.append("MIME-Version: 1").append(lf);
377 writer.append(lf).append("This is a message with multiple parts in MIME format.").append(lf);
379 for (AppVersionUpload.FileInfo fileInfo : payload) {
381 File binaryFile = fileInfo.file;
382 writer.append("--" + boundary).append(lf);
383 writer.append("X-Appcfg-File: ").append(URLEncoder.encode(fileInfo.path, "UTF-8"))
384 .append(lf);
385 writer.append("X-Appcfg-Hash: ").append(fileInfo.hash).append(lf);
386 String mimeValue = fileInfo.mimeType;
387 if (mimeValue == null) {
388 mimeValue = Application.guessContentTypeFromName(binaryFile.getName());
390 writer.append("Content-Type: " + mimeValue).append(lf);
391 writer.append("Content-Length: ").append("" + binaryFile.length()).append(lf);
392 writer.append("Content-Transfer-Encoding: 8bit").append(lf);
394 writer.append(lf).flush();
395 FilePoster filePoster = new FilePoster(binaryFile);
396 filePoster.post(output);
397 writer.append(lf).flush();
399 writer.append("--" + boundary + "--").append(lf);
400 writer.append(lf).flush();
402 } finally {
403 if (writer != null) {
404 writer.close();