App Engine Java SDK version 1.9.10
[gae.git] / java / src / main / com / google / appengine / tools / admin / AbstractServerConnection.java
blob06d06d31927a8a290e20bb638f722151baffd8b8
1 // Copyright 2009 Google Inc. All rights reserved.
3 package com.google.appengine.tools.admin;
5 import com.google.appengine.tools.admin.AppAdminFactory.ConnectOptions;
6 import com.google.common.collect.Multimap;
7 import com.google.common.collect.Multimaps;
9 import java.io.BufferedReader;
10 import java.io.File;
11 import java.io.FileInputStream;
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.io.InputStreamReader;
15 import java.io.OutputStream;
16 import java.io.OutputStreamWriter;
17 import java.io.PrintWriter;
18 import java.io.StringReader;
19 import java.io.UnsupportedEncodingException;
20 import java.net.HttpURLConnection;
21 import java.net.MalformedURLException;
22 import java.net.URL;
23 import java.net.URLEncoder;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.logging.Logger;
29 /**
30 * Connection to the AppEngine hosting service, as set by {@link ConnectOptions}
33 public abstract class AbstractServerConnection implements ServerConnection {
35 private static final int MAX_SEND_RETRIES = 3;
37 protected interface DataPoster {
38 void post(OutputStream s) throws IOException;
41 private static class FilePoster implements DataPoster {
42 private static final int BUFFER_SIZE = 4 * 1024;
43 private File file;
45 public FilePoster(File file) {
46 assert (file != null && file.exists());
47 this.file = file;
50 @Override
51 public void post(OutputStream out) throws IOException {
52 InputStream in = new FileInputStream(file);
53 try {
54 byte[] buf = new byte[BUFFER_SIZE];
55 int len;
56 while ((len = in.read(buf)) != -1) {
57 out.write(buf, 0, len);
59 } finally {
60 in.close();
65 static class StringPoster implements DataPoster {
66 private String str;
68 public StringPoster(String s) {
69 str = s;
72 @Override
73 public void post(OutputStream s) throws IOException {
74 s.write(str.getBytes("UTF-8"));
78 protected static final String POST = "POST";
80 protected static final String GET = "GET";
82 protected ConnectOptions options;
84 protected static Logger logger =
85 Logger.getLogger(AbstractServerConnection.class.getCanonicalName());
87 protected AbstractServerConnection() {
90 protected AbstractServerConnection(ConnectOptions options) {
91 this.options = options;
92 if (System.getProperty("http.proxyHost") != null) {
93 logger.info("proxying HTTP through " + System.getProperty("http.proxyHost") + ":"
94 + System.getProperty("http.proxyPort"));
96 if (System.getProperty("https.proxyHost") != null) {
97 logger.info("proxying HTTPS through " + System.getProperty("https.proxyHost") + ":"
98 + System.getProperty("https.proxyPort"));
102 protected String buildQuery(Map<String, String> params) throws UnsupportedEncodingException {
103 return buildQuery(Multimaps.forMap(params));
106 protected String buildQuery(Multimap<String, String> params) throws UnsupportedEncodingException {
107 StringBuffer buf = new StringBuffer();
108 for (String key : params.keySet()) {
109 String encodedKey = URLEncoder.encode(key, "UTF-8");
110 for (String value : params.get(key)) {
111 buf.append(encodedKey);
112 buf.append('=');
113 buf.append(URLEncoder.encode(value, "UTF-8"));
114 buf.append('&');
117 return buf.toString();
120 protected URL buildURL(String path) throws MalformedURLException {
121 String protocol = options.getSecure() ? "https" : "http";
122 return new URL(protocol + "://" + options.getServer() + path);
125 protected IOException connect(String method, HttpURLConnection conn, DataPoster data)
126 throws IOException {
127 doPreConnect(method, conn, data);
128 conn.setInstanceFollowRedirects(false);
129 conn.setRequestMethod(method);
131 if (POST.equals(method)) {
132 conn.setDoOutput(true);
133 OutputStream out = conn.getOutputStream();
134 if (data != null) {
135 data.post(out);
137 out.close();
140 try {
141 conn.getInputStream();
142 } catch (IOException ex) {
143 return ex;
144 } finally {
145 doPostConnect(method, conn, data);
148 return null;
151 protected String constructHttpErrorMessage(HttpURLConnection conn, BufferedReader reader)
152 throws IOException {
153 StringBuilder sb = new StringBuilder("Error posting to URL: ");
154 sb.append(conn.getURL());
155 sb.append('\n');
156 sb.append(conn.getResponseCode());
157 sb.append(' ');
158 sb.append(conn.getResponseMessage());
159 sb.append('\n');
160 if (reader != null) {
161 for (String line; (line = reader.readLine()) != null;) {
162 sb.append(line);
163 sb.append('\n');
166 return sb.toString();
169 protected abstract void doHandleSendErrors(int status, URL url, HttpURLConnection conn,
170 BufferedReader connReader) throws IOException;
172 protected abstract void doPostConnect(String method, HttpURLConnection conn, DataPoster data)
173 throws IOException;
175 protected abstract void doPreConnect(String method, HttpURLConnection conn, DataPoster data)
176 throws IOException;
178 @Override
179 public String get(String url, Map<String, String> params) throws IOException {
180 return send(GET, url, null, null, Multimaps.forMap(params));
183 @Override
184 public String get(String url, Multimap<String, String> params) throws IOException {
185 return send(GET, url, null, null, params);
188 private static InputStream getInputStream(HttpURLConnection conn) {
189 InputStream is;
190 try {
191 return conn.getInputStream();
192 } catch (IOException ex) {
193 return conn.getErrorStream();
197 private static BufferedReader getReader(InputStream is) {
198 if (is == null) {
199 return null;
201 return new BufferedReader(new InputStreamReader(is));
204 protected static String getString(InputStream is) throws IOException {
205 StringBuffer response = new StringBuffer();
206 InputStreamReader reader = new InputStreamReader(is);
207 char buffer[] = new char[8192];
208 int read = 0;
209 while ((read = reader.read(buffer)) != -1) {
210 response.append(buffer, 0, read);
212 return response.toString();
215 protected static BufferedReader getReader(HttpURLConnection conn) {
216 return getReader(getInputStream(conn));
219 @Override
220 public String post(String url, File payload, String contentType, Map<String, String> params)
221 throws IOException {
222 return send(POST, url, new FilePoster(payload), contentType, Multimaps.forMap(params));
225 @Override
226 public String post(String url, File payload, String contentType, Multimap<String, String> params)
227 throws IOException {
228 return send(POST, url, new FilePoster(payload), contentType, params);
231 protected InputStream postAndGetInputStream(String url, DataPoster poster, String... params)
232 throws IOException {
233 return send1(POST, url, poster, null, Multimaps.forMap(getParamMap(params)));
236 @Override
237 public String post(String url, File payload, String contentType, String... params)
238 throws IOException {
239 return send(POST, url, new FilePoster(payload), contentType,
240 Multimaps.forMap(getParamMap(params)));
243 @Override
244 public String post(String url, List<AppVersionUpload.FileInfo> payload, String... params)
245 throws IOException {
246 return sendBatch(POST, url, payload, getParamMap(params));
249 @Override
250 public String post(String url, String payload, Map<String, String> params)
251 throws IOException {
252 return send(POST, url, new StringPoster(payload), null, Multimaps.forMap(params));
255 @Override
256 public String post(String url, String payload, Multimap<String, String> params)
257 throws IOException {
258 return send(POST, url, new StringPoster(payload), null, params);
261 @Override
262 public String post(String url, String payload, String... params) throws IOException {
263 return send(POST, url, new StringPoster(payload), null, Multimaps.forMap(getParamMap(params)));
267 * Returns an input stream that reads from this open connection. If the server returned
268 * an error (404, etc), return the error stream instead.
270 * @return {@code InputStream} for the server response.
272 @Override
273 public InputStream postAndGetInputStream(String url, String payload, String... params)
274 throws IOException {
275 return send1(POST, url, new StringPoster(payload), null, Multimaps.forMap(getParamMap(params)));
278 protected HttpURLConnection openConnection(URL url) throws IOException {
279 return (HttpURLConnection) url.openConnection();
282 protected String send(String method, String path, DataPoster payload, String content_type,
283 Multimap<String, String> params) throws IOException {
284 return getString(send1(method, path, payload, content_type, params));
287 private Map<String, String> getParamMap(String... params) {
288 Map<String, String> paramMap = new HashMap<String, String>();
289 for (int i = 0; i < params.length; i += 2) {
290 paramMap.put(params[i], params[i + 1]);
292 return paramMap;
295 private InputStream send1(String method, String path, DataPoster payload, String content_type,
296 Multimap<String, String> params) throws IOException {
298 URL url = buildURL(path + '?' + buildQuery(params));
300 if (content_type == null) {
301 content_type = Application.guessContentTypeFromName(path);
304 int tries = 0;
305 while (true) {
306 HttpURLConnection conn = openConnection(url);
307 conn.setRequestProperty("Content-type", content_type);
308 conn.setRequestProperty("X-appcfg-api-version", "1");
310 if (options.getHost() != null) {
311 conn.setRequestProperty("Host", options.getHost());
314 IOException ioe = connect(method, conn, payload);
316 int status = conn.getResponseCode();
318 if (status == HttpURLConnection.HTTP_OK) {
319 return getInputStream(conn);
320 } else {
321 BufferedReader reader = getReader(conn);
322 StringBuilder sb = new StringBuilder();
323 for (String line; (line = reader.readLine()) != null;) {
324 sb.append(line);
326 String response = sb.toString();
327 String httpErrorMessage = constructHttpErrorMessage(
328 conn,
329 new BufferedReader(new StringReader(response)));
330 logger.warning(httpErrorMessage + "This is try #" + tries);
331 if (++tries > MAX_SEND_RETRIES) {
332 throw new HttpIoException(httpErrorMessage, status);
334 doHandleSendErrors(status, url, conn, new BufferedReader(new StringReader(response)));
339 protected String sendBatch(String method, String path, List<AppVersionUpload.FileInfo> payload,
340 Map<String, String> params) throws IOException {
341 return getString(sendBatchPayload(method, path, payload, params));
344 private InputStream sendBatchPayload(String method, String path,
345 List<AppVersionUpload.FileInfo> payload,
346 Map<String, String> params) throws IOException {
348 URL url = buildURL(path + '?' + buildQuery(params));
350 int tries = 0;
351 while (true) {
352 String boundary = "boundary" + Long
353 .toHexString(System.currentTimeMillis());
354 HttpURLConnection conn = openConnection(url);
355 conn.setRequestProperty("MIME-Version", "1.0");
356 conn.setRequestProperty("Content-Type", "message/rfc822");
357 conn.setRequestProperty("X-appcfg-api-version", "1");
358 doPreConnect(method, conn, null);
359 conn.setDoOutput(true);
360 conn.setRequestMethod(method);
362 if (options.getHost() != null) {
363 conn.setRequestProperty("Host", options.getHost());
365 conn.setInstanceFollowRedirects(false);
367 populateMixedPayloadStream(conn.getOutputStream(), payload, boundary);
369 doPostConnect(method, conn, null);
371 int status = conn.getResponseCode();
372 if (status == HttpURLConnection.HTTP_OK) {
373 return getInputStream(conn);
374 } else {
375 BufferedReader reader = getReader(conn);
376 StringBuilder sb = new StringBuilder();
377 for (String line; (line = reader.readLine()) != null;) {
378 sb.append(line);
380 String response = sb.toString();
381 String httpErrorMessage = constructHttpErrorMessage(
382 conn,
383 new BufferedReader(new StringReader(response)));
384 logger.warning(httpErrorMessage + "This is try #" + tries);
385 if (++tries > MAX_SEND_RETRIES) {
386 throw new IOException(httpErrorMessage);
388 doHandleSendErrors(status, url, conn, new BufferedReader(new StringReader(response)));
394 void populateMixedPayloadStream(OutputStream output,
395 List<AppVersionUpload.FileInfo> payload, String boundary) throws IOException {
397 String charset = "UTF-8";
398 String lf = "\n";
399 PrintWriter writer = null;
400 try {
401 writer = new PrintWriter(new OutputStreamWriter(output, charset),
402 true);
403 writer.append("Content-Type: multipart/mixed; boundary=\"" + boundary + "\"").append(lf);
404 writer.append("MIME-Version: 1").append(lf);
405 writer.append(lf).append("This is a message with multiple parts in MIME format.").append(lf);
407 for (AppVersionUpload.FileInfo fileInfo : payload) {
409 File binaryFile = fileInfo.file;
410 writer.append("--" + boundary).append(lf);
411 writer.append("X-Appcfg-File: ").append(URLEncoder.encode(fileInfo.path, "UTF-8"))
412 .append(lf);
413 writer.append("X-Appcfg-Hash: ").append(fileInfo.hash).append(lf);
414 String mimeValue = fileInfo.mimeType;
415 if (mimeValue == null) {
416 mimeValue = Application.guessContentTypeFromName(binaryFile.getName());
418 writer.append("Content-Type: " + mimeValue).append(lf);
419 writer.append("Content-Length: ").append("" + binaryFile.length()).append(lf);
420 writer.append("Content-Transfer-Encoding: 8bit").append(lf);
422 writer.append(lf).flush();
423 FilePoster filePoster = new FilePoster(binaryFile);
424 filePoster.post(output);
425 writer.append(lf).flush();
427 writer.append("--" + boundary + "--").append(lf);
428 writer.append(lf).flush();
430 } finally {
431 if (writer != null) {
432 writer.close();