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
;
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
;
23 import java
.net
.URLEncoder
;
24 import java
.util
.HashMap
;
25 import java
.util
.List
;
27 import java
.util
.logging
.Logger
;
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;
45 public FilePoster(File file
) {
46 assert (file
!= null && file
.exists());
51 public void post(OutputStream out
) throws IOException
{
52 InputStream in
= new FileInputStream(file
);
54 byte[] buf
= new byte[BUFFER_SIZE
];
56 while ((len
= in
.read(buf
)) != -1) {
57 out
.write(buf
, 0, len
);
65 static class StringPoster
implements DataPoster
{
68 public StringPoster(String s
) {
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
);
113 buf
.append(URLEncoder
.encode(value
, "UTF-8"));
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
)
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();
141 conn
.getInputStream();
142 } catch (IOException ex
) {
145 doPostConnect(method
, conn
, data
);
151 protected String
constructHttpErrorMessage(HttpURLConnection conn
, BufferedReader reader
)
153 StringBuilder sb
= new StringBuilder("Error posting to URL: ");
154 sb
.append(conn
.getURL());
156 sb
.append(conn
.getResponseCode());
158 sb
.append(conn
.getResponseMessage());
160 if (reader
!= null) {
161 for (String line
; (line
= reader
.readLine()) != null;) {
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
)
175 protected abstract void doPreConnect(String method
, HttpURLConnection conn
, DataPoster data
)
179 public String
get(String url
, Map
<String
, String
> params
) throws IOException
{
180 return send(GET
, url
, null, null, Multimaps
.forMap(params
));
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
) {
191 return conn
.getInputStream();
192 } catch (IOException ex
) {
193 return conn
.getErrorStream();
197 private static BufferedReader
getReader(InputStream is
) {
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];
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
));
220 public String
post(String url
, File payload
, String contentType
, Map
<String
, String
> params
)
222 return send(POST
, url
, new FilePoster(payload
), contentType
, Multimaps
.forMap(params
));
226 public String
post(String url
, File payload
, String contentType
, Multimap
<String
, String
> params
)
228 return send(POST
, url
, new FilePoster(payload
), contentType
, params
);
231 protected InputStream
postAndGetInputStream(String url
, DataPoster poster
, String
... params
)
233 return send1(POST
, url
, poster
, null, Multimaps
.forMap(getParamMap(params
)));
237 public String
post(String url
, File payload
, String contentType
, String
... params
)
239 return send(POST
, url
, new FilePoster(payload
), contentType
,
240 Multimaps
.forMap(getParamMap(params
)));
244 public String
post(String url
, List
<AppVersionUpload
.FileInfo
> payload
, String
... params
)
246 return sendBatch(POST
, url
, payload
, getParamMap(params
));
250 public String
post(String url
, String payload
, Map
<String
, String
> params
)
252 return send(POST
, url
, new StringPoster(payload
), null, Multimaps
.forMap(params
));
256 public String
post(String url
, String payload
, Multimap
<String
, String
> params
)
258 return send(POST
, url
, new StringPoster(payload
), null, params
);
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.
273 public InputStream
postAndGetInputStream(String url
, String payload
, String
... params
)
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]);
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
);
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
);
321 BufferedReader reader
= getReader(conn
);
322 StringBuilder sb
= new StringBuilder();
323 for (String line
; (line
= reader
.readLine()) != null;) {
326 String response
= sb
.toString();
327 String httpErrorMessage
= constructHttpErrorMessage(
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
));
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
);
375 BufferedReader reader
= getReader(conn
);
376 StringBuilder sb
= new StringBuilder();
377 for (String line
; (line
= reader
.readLine()) != null;) {
380 String response
= sb
.toString();
381 String httpErrorMessage
= constructHttpErrorMessage(
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";
399 PrintWriter writer
= null;
401 writer
= new PrintWriter(new OutputStreamWriter(output
, charset
),
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"))
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();
431 if (writer
!= null) {