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;
36 private static final String USER_AGENT_HEADER
= "X-appcfg-user-agent";
37 private static final String USER_AGENT_KEY
= "appengine.useragent";
39 protected interface DataPoster
{
40 void post(OutputStream s
) throws IOException
;
43 private static class FilePoster
implements DataPoster
{
44 private static final int BUFFER_SIZE
= 4 * 1024;
47 public FilePoster(File file
) {
48 assert (file
!= null && file
.exists());
53 public void post(OutputStream out
) throws IOException
{
54 InputStream in
= new FileInputStream(file
);
56 byte[] buf
= new byte[BUFFER_SIZE
];
58 while ((len
= in
.read(buf
)) != -1) {
59 out
.write(buf
, 0, len
);
67 static class StringPoster
implements DataPoster
{
70 public StringPoster(String s
) {
75 public void post(OutputStream s
) throws IOException
{
76 s
.write(str
.getBytes("UTF-8"));
80 protected static final String POST
= "POST";
82 protected static final String GET
= "GET";
84 protected ConnectOptions options
;
86 protected static Logger logger
=
87 Logger
.getLogger(AbstractServerConnection
.class.getCanonicalName());
89 protected AbstractServerConnection() {
92 protected AbstractServerConnection(ConnectOptions options
) {
93 this.options
= options
;
94 if (System
.getProperty("http.proxyHost") != null) {
95 logger
.info("proxying HTTP through " + System
.getProperty("http.proxyHost") + ":"
96 + System
.getProperty("http.proxyPort"));
98 if (System
.getProperty("https.proxyHost") != null) {
99 logger
.info("proxying HTTPS through " + System
.getProperty("https.proxyHost") + ":"
100 + System
.getProperty("https.proxyPort"));
104 protected String
buildQuery(Map
<String
, String
> params
) throws UnsupportedEncodingException
{
105 return buildQuery(Multimaps
.forMap(params
));
108 protected String
buildQuery(Multimap
<String
, String
> params
) throws UnsupportedEncodingException
{
109 StringBuffer buf
= new StringBuffer();
110 for (String key
: params
.keySet()) {
111 String encodedKey
= URLEncoder
.encode(key
, "UTF-8");
112 for (String value
: params
.get(key
)) {
113 buf
.append(encodedKey
);
115 buf
.append(URLEncoder
.encode(value
, "UTF-8"));
119 return buf
.toString();
122 protected URL
buildURL(String path
) throws MalformedURLException
{
123 String protocol
= options
.getSecure() ?
"https" : "http";
124 return new URL(protocol
+ "://" + options
.getServer() + path
);
127 protected IOException
connect(String method
, HttpURLConnection conn
, DataPoster data
)
129 doPreConnect(method
, conn
, data
);
130 conn
.setInstanceFollowRedirects(false);
131 conn
.setRequestMethod(method
);
133 if (POST
.equals(method
)) {
134 conn
.setDoOutput(true);
135 OutputStream out
= conn
.getOutputStream();
143 conn
.getInputStream();
144 } catch (IOException ex
) {
147 doPostConnect(method
, conn
, data
);
153 protected String
constructHttpErrorMessage(HttpURLConnection conn
, BufferedReader reader
)
155 StringBuilder sb
= new StringBuilder("Error posting to URL: ");
156 sb
.append(conn
.getURL());
158 sb
.append(conn
.getResponseCode());
160 sb
.append(conn
.getResponseMessage());
162 if (reader
!= null) {
163 for (String line
; (line
= reader
.readLine()) != null;) {
168 return sb
.toString();
171 protected abstract void doHandleSendErrors(int status
, URL url
, HttpURLConnection conn
,
172 BufferedReader connReader
) throws IOException
;
174 protected abstract void doPostConnect(String method
, HttpURLConnection conn
, DataPoster data
)
177 protected abstract void doPreConnect(String method
, HttpURLConnection conn
, DataPoster data
)
181 public String
get(String url
, Map
<String
, String
> params
) throws IOException
{
182 return send(GET
, url
, null, null, Multimaps
.forMap(params
));
186 public String
get(String url
, Multimap
<String
, String
> params
) throws IOException
{
187 return send(GET
, url
, null, null, params
);
190 private static InputStream
getInputStream(HttpURLConnection conn
) {
193 return conn
.getInputStream();
194 } catch (IOException ex
) {
195 return conn
.getErrorStream();
199 private static BufferedReader
getReader(InputStream is
) {
203 return new BufferedReader(new InputStreamReader(is
));
206 protected static String
getString(InputStream is
) throws IOException
{
207 StringBuffer response
= new StringBuffer();
208 InputStreamReader reader
= new InputStreamReader(is
);
209 char[] buffer
= new char[8192];
211 while ((read
= reader
.read(buffer
)) != -1) {
212 response
.append(buffer
, 0, read
);
214 return response
.toString();
217 protected static BufferedReader
getReader(HttpURLConnection conn
) {
218 return getReader(getInputStream(conn
));
222 public String
post(String url
, File payload
, String contentType
, Map
<String
, String
> params
)
224 return send(POST
, url
, new FilePoster(payload
), contentType
, Multimaps
.forMap(params
));
228 public String
post(String url
, File payload
, String contentType
, Multimap
<String
, String
> params
)
230 return send(POST
, url
, new FilePoster(payload
), contentType
, params
);
233 protected InputStream
postAndGetInputStream(String url
, DataPoster poster
, String
... params
)
235 return send1(POST
, url
, poster
, null, Multimaps
.forMap(getParamMap(params
)));
239 public String
post(String url
, File payload
, String contentType
, String
... params
)
241 return send(POST
, url
, new FilePoster(payload
), contentType
,
242 Multimaps
.forMap(getParamMap(params
)));
246 public String
post(String url
, List
<AppVersionUpload
.FileInfo
> payload
, String
... params
)
248 return sendBatch(POST
, url
, payload
, getParamMap(params
));
252 public String
post(String url
, String payload
, Map
<String
, String
> params
)
254 return send(POST
, url
, new StringPoster(payload
), null, Multimaps
.forMap(params
));
258 public String
post(String url
, String payload
, Multimap
<String
, String
> params
)
260 return send(POST
, url
, new StringPoster(payload
), null, params
);
264 public String
post(String url
, String payload
, String
... params
) throws IOException
{
265 return send(POST
, url
, new StringPoster(payload
), null, Multimaps
.forMap(getParamMap(params
)));
269 * Returns an input stream that reads from this open connection. If the server returned
270 * an error (404, etc), return the error stream instead.
272 * @return {@code InputStream} for the server response.
275 public InputStream
postAndGetInputStream(String url
, String payload
, String
... params
)
277 return send1(POST
, url
, new StringPoster(payload
), null, Multimaps
.forMap(getParamMap(params
)));
280 protected HttpURLConnection
openConnection(URL url
) throws IOException
{
281 return (HttpURLConnection
) url
.openConnection();
284 protected String
send(String method
, String path
, DataPoster payload
, String content_type
,
285 Multimap
<String
, String
> params
) throws IOException
{
286 return getString(send1(method
, path
, payload
, content_type
, params
));
289 private Map
<String
, String
> getParamMap(String
... params
) {
290 Map
<String
, String
> paramMap
= new HashMap
<String
, String
>();
291 for (int i
= 0; i
< params
.length
; i
+= 2) {
292 paramMap
.put(params
[i
], params
[i
+ 1]);
297 private InputStream
send1(String method
, String path
, DataPoster payload
, String content_type
,
298 Multimap
<String
, String
> params
) throws IOException
{
300 URL url
= buildURL(path
+ '?' + buildQuery(params
));
302 if (content_type
== null) {
303 content_type
= Application
.guessContentTypeFromName(path
);
308 HttpURLConnection conn
= openConnection(url
);
309 conn
.setRequestProperty("Content-type", content_type
);
310 conn
.setRequestProperty("X-appcfg-api-version", "1");
312 String userAgentValue
= System
.getProperty(USER_AGENT_KEY
);
313 if (userAgentValue
!= null) {
314 conn
.setRequestProperty(USER_AGENT_HEADER
, userAgentValue
);
317 if (options
.getHost() != null) {
318 conn
.setRequestProperty("Host", options
.getHost());
321 IOException ioe
= connect(method
, conn
, payload
);
323 int status
= conn
.getResponseCode();
325 if (status
== HttpURLConnection
.HTTP_OK
) {
326 return getInputStream(conn
);
328 BufferedReader reader
= getReader(conn
);
329 StringBuilder sb
= new StringBuilder();
330 for (String line
; (line
= reader
.readLine()) != null;) {
333 String response
= sb
.toString();
334 String httpErrorMessage
= constructHttpErrorMessage(
336 new BufferedReader(new StringReader(response
)));
337 logger
.warning(httpErrorMessage
+ "This is try #" + tries
);
338 if (++tries
> MAX_SEND_RETRIES
) {
339 throw new HttpIoException(httpErrorMessage
, status
);
341 doHandleSendErrors(status
, url
, conn
, new BufferedReader(new StringReader(response
)));
346 protected String
sendBatch(String method
, String path
, List
<AppVersionUpload
.FileInfo
> payload
,
347 Map
<String
, String
> params
) throws IOException
{
348 return getString(sendBatchPayload(method
, path
, payload
, params
));
351 private InputStream
sendBatchPayload(String method
, String path
,
352 List
<AppVersionUpload
.FileInfo
> payload
,
353 Map
<String
, String
> params
) throws IOException
{
355 URL url
= buildURL(path
+ '?' + buildQuery(params
));
359 String boundary
= "boundary" + Long
360 .toHexString(System
.currentTimeMillis());
361 HttpURLConnection conn
= openConnection(url
);
362 conn
.setRequestProperty("MIME-Version", "1.0");
363 conn
.setRequestProperty("Content-Type", "message/rfc822");
364 conn
.setRequestProperty("X-appcfg-api-version", "1");
365 doPreConnect(method
, conn
, null);
366 conn
.setDoOutput(true);
367 conn
.setRequestMethod(method
);
369 if (options
.getHost() != null) {
370 conn
.setRequestProperty("Host", options
.getHost());
372 conn
.setInstanceFollowRedirects(false);
374 populateMixedPayloadStream(conn
.getOutputStream(), payload
, boundary
);
376 doPostConnect(method
, conn
, null);
378 int status
= conn
.getResponseCode();
379 if (status
== HttpURLConnection
.HTTP_OK
) {
380 return getInputStream(conn
);
382 BufferedReader reader
= getReader(conn
);
383 StringBuilder sb
= new StringBuilder();
384 for (String line
; (line
= reader
.readLine()) != null;) {
387 String response
= sb
.toString();
388 String httpErrorMessage
= constructHttpErrorMessage(
390 new BufferedReader(new StringReader(response
)));
391 logger
.warning(httpErrorMessage
+ "This is try #" + tries
);
392 if (++tries
> MAX_SEND_RETRIES
) {
393 throw new IOException(httpErrorMessage
);
395 doHandleSendErrors(status
, url
, conn
, new BufferedReader(new StringReader(response
)));
401 void populateMixedPayloadStream(OutputStream output
,
402 List
<AppVersionUpload
.FileInfo
> payload
, String boundary
) throws IOException
{
404 String charset
= "UTF-8";
406 PrintWriter writer
= null;
408 writer
= new PrintWriter(new OutputStreamWriter(output
, charset
),
410 writer
.append("Content-Type: multipart/mixed; boundary=\"" + boundary
+ "\"").append(lf
);
411 writer
.append("MIME-Version: 1").append(lf
);
412 writer
.append(lf
).append("This is a message with multiple parts in MIME format.").append(lf
);
414 for (AppVersionUpload
.FileInfo fileInfo
: payload
) {
416 File binaryFile
= fileInfo
.file
;
417 writer
.append("--" + boundary
).append(lf
);
418 writer
.append("X-Appcfg-File: ").append(URLEncoder
.encode(fileInfo
.path
, "UTF-8"))
420 writer
.append("X-Appcfg-Hash: ").append(fileInfo
.hash
).append(lf
);
421 String mimeValue
= fileInfo
.mimeType
;
422 if (mimeValue
== null) {
423 mimeValue
= Application
.guessContentTypeFromName(binaryFile
.getName());
425 writer
.append("Content-Type: " + mimeValue
).append(lf
);
426 writer
.append("Content-Length: ").append("" + binaryFile
.length()).append(lf
);
427 writer
.append("Content-Transfer-Encoding: 8bit").append(lf
);
429 writer
.append(lf
).flush();
430 FilePoster filePoster
= new FilePoster(binaryFile
);
431 filePoster
.post(output
);
432 writer
.append(lf
).flush();
434 writer
.append("--" + boundary
+ "--").append(lf
);
435 writer
.append(lf
).flush();
438 if (writer
!= null) {