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
;
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
;
21 import java
.net
.URLEncoder
;
22 import java
.util
.HashMap
;
23 import java
.util
.List
;
25 import java
.util
.logging
.Logger
;
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;
43 public FilePoster(File file
) {
44 assert (file
!= null && file
.exists());
49 public void post(OutputStream out
) throws IOException
{
50 InputStream in
= new FileInputStream(file
);
52 byte[] buf
= new byte[BUFFER_SIZE
];
54 while ((len
= in
.read(buf
)) != -1) {
55 out
.write(buf
, 0, len
);
63 static class StringPoster
implements DataPoster
{
66 public StringPoster(String s
) {
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"));
105 buf
.append(URLEncoder
.encode(params
.get(key
), "UTF-8"));
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
)
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();
132 conn
.getInputStream();
133 } catch (IOException ex
) {
136 doPostConnect(method
, conn
, data
);
142 protected String
constructHttpErrorMessage(HttpURLConnection conn
, BufferedReader reader
)
144 StringBuilder sb
= new StringBuilder("Error posting to URL: ");
145 sb
.append(conn
.getURL());
147 sb
.append(conn
.getResponseCode());
149 sb
.append(conn
.getResponseMessage());
151 if (reader
!= null) {
152 for (String line
; (line
= reader
.readLine()) != null;) {
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
)
166 protected abstract void doPreConnect(String method
, HttpURLConnection conn
, DataPoster data
)
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
) {
177 return conn
.getInputStream();
178 } catch (IOException ex
) {
179 return conn
.getErrorStream();
183 private static BufferedReader
getReader(InputStream is
) {
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];
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
));
206 public String
post(String url
, File payload
, String contentType
, Map
<String
, String
> params
)
208 return send(POST
, url
, new FilePoster(payload
), contentType
, params
);
211 protected InputStream
postAndGetInputStream(String url
, DataPoster poster
, String
... params
)
213 return send1(POST
, url
, poster
, null, getParamMap(params
));
217 public String
post(String url
, File payload
, String contentType
, String
... params
)
219 return send(POST
, url
, new FilePoster(payload
), contentType
, getParamMap(params
));
223 public String
post(String url
, List
<AppVersionUpload
.FileInfo
> payload
, String
... params
)
225 return sendBatch(POST
, url
, payload
, getParamMap(params
));
229 public String
post(String url
, String payload
, Map
<String
, String
> params
) throws IOException
{
230 return send(POST
, url
, new StringPoster(payload
), null, params
);
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.
245 public InputStream
postAndGetInputStream(String url
, String payload
, String
... params
)
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]);
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
);
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
);
293 BufferedReader reader
= getReader(conn
);
294 StringBuilder sb
= new StringBuilder();
295 for (String line
; (line
= reader
.readLine()) != null;) {
298 String response
= sb
.toString();
299 String httpErrorMessage
= constructHttpErrorMessage(
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
));
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
);
347 BufferedReader reader
= getReader(conn
);
348 StringBuilder sb
= new StringBuilder();
349 for (String line
; (line
= reader
.readLine()) != null;) {
352 String response
= sb
.toString();
353 String httpErrorMessage
= constructHttpErrorMessage(
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";
371 PrintWriter writer
= null;
373 writer
= new PrintWriter(new OutputStreamWriter(output
, charset
),
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"))
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();
403 if (writer
!= null) {