2 * Copyright (C) 2011 Morphoss Ltd
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 package com
.morphoss
.acal
.service
.connector
;
21 import java
.io
.BufferedReader
;
22 import java
.io
.ByteArrayInputStream
;
23 import java
.io
.InputStream
;
24 import java
.io
.InputStreamReader
;
25 import java
.net
.InetAddress
;
26 import java
.net
.SocketException
;
27 import java
.net
.UnknownHostException
;
28 import java
.security
.MessageDigest
;
29 import java
.security
.NoSuchAlgorithmException
;
30 import java
.util
.regex
.Matcher
;
31 import java
.util
.regex
.Pattern
;
33 import javax
.net
.ssl
.SSLException
;
35 import org
.apache
.http
.Header
;
36 import org
.apache
.http
.HeaderElement
;
37 import org
.apache
.http
.HttpEntity
;
38 import org
.apache
.http
.HttpHost
;
39 import org
.apache
.http
.HttpResponse
;
40 import org
.apache
.http
.client
.HttpClient
;
41 import org
.apache
.http
.conn
.ConnectTimeoutException
;
42 import org
.apache
.http
.conn
.ConnectionPoolTimeoutException
;
43 import org
.apache
.http
.entity
.StringEntity
;
44 import org
.apache
.http
.impl
.client
.DefaultHttpClient
;
45 import org
.apache
.http
.impl
.conn
.tsccm
.ThreadSafeClientConnManager
;
46 import org
.apache
.http
.message
.BasicHeader
;
47 import org
.apache
.http
.params
.HttpParams
;
49 import android
.content
.ContentValues
;
50 import android
.net
.Uri
;
51 import android
.util
.Log
;
53 import com
.morphoss
.acal
.AcalDebug
;
54 import com
.morphoss
.acal
.Constants
;
55 import com
.morphoss
.acal
.StaticHelpers
;
56 import com
.morphoss
.acal
.activity
.serverconfig
.AuthenticationFailure
;
57 import com
.morphoss
.acal
.providers
.Servers
;
58 import com
.morphoss
.acal
.xml
.DavNode
;
59 import com
.morphoss
.acal
.xml
.DavParserFactory
;
61 public class AcalRequestor
{
63 final private static String TAG
= "AcalRequestor";
65 private static final int LONG_LINE_WRAP_FOR_DEBUG
= 500;
67 private boolean initialised
= false;
69 // Basic URI components
70 private String hostName
= null;
71 private String path
= "/";
72 private String protocol
= "http";
74 private String method
= "PROPFIND";
76 // Authentication crap.
77 protected boolean authRequired
= false;
78 protected int authType
= Servers
.AUTH_NONE
;
79 protected Header wwwAuthenticate
= null;
80 protected String authRealm
= null;
81 protected String nonce
= null;
82 protected String opaque
= null;
83 protected String cnonce
= null;
84 protected String qop
= null;
85 protected int authNC
= 0;
86 protected String algorithm
= null;
88 private String username
= null;
89 private String password
= null;
91 private static String userAgent
= null;
93 private HttpParams httpParams
;
94 private HttpClient httpClient
;
95 private ThreadSafeClientConnManager connManager
;
96 private Header responseHeaders
[];
97 private int statusCode
= -1;
98 private int connectionTimeOut
= 30000;
99 private int socketTimeOut
= 60000;
100 private int redirectLimit
= 5;
101 private int redirectCount
= 0;
105 * Construct an uninitialised AcalRequestor. After calling this you will need to
106 * initialise things by either calling setFromServer() or interpretUriString() before
107 * you will be able to make a request.
109 public AcalRequestor() {
113 * Construct a new contentvalues from these path components.
121 public AcalRequestor( String hostIn
, Integer proto
, Integer portIn
, String pathIn
, String user
, String pass
) {
123 setPortProtocol(portIn
,proto
);
132 * Construct a new AcalRequestor from the values in a ContentValues which has been read
133 * from a Server row in the database. In this case the hostname / path will be set from
134 * the 'simple' configuration values.
135 * @param cvServerData
138 public static AcalRequestor
fromSimpleValues( ContentValues cvServerData
) {
139 AcalRequestor result
= new AcalRequestor();
141 result
.protocol
= "http";
142 result
.hostName
= null;
146 result
.authType
= Servers
.AUTH_NONE
;
147 result
.interpretUriString(cvServerData
.getAsString(Servers
.SUPPLIED_USER_URL
));
149 if ( result
.hostName
== null ) result
.hostName
= "invalid";
150 if ( result
.path
== null ) result
.path
= "/";
152 if ( !result
.initialised
) result
.initialise();
159 * Construct a new AcalRequestor from the values in a ContentValues which has been read
160 * from a Server row in the database. The path will be set to the principal-path value
161 * so you may need to specify a different path on the actual request(s).
162 * @param cvServerData
165 public static AcalRequestor
fromServerValues( ContentValues cvServerData
) {
166 AcalRequestor result
= new AcalRequestor();
167 result
.applyFromServer(cvServerData
);
173 * Adjust the current URI values to align with those in a ContentValues which has been read
174 * from a Server row in the database. The path will be set to the principal-path value
175 * so you may need to specify a different path on the actual request(s)
176 * @param cvServerData
177 * @param simpleSetup true/false whether to use only the 'simple' values to initialise from
179 public void applyFromServer( ContentValues cvServerData
) {
180 setHostName(cvServerData
.getAsString(Servers
.HOSTNAME
));
181 setPath(cvServerData
.getAsString(Servers
.PRINCIPAL_PATH
));
183 String portString
= cvServerData
.getAsString(Servers
.PORT
);
185 if ( portString
!= null && portString
.length() > 0 ) tmpPort
= Integer
.parseInt(portString
);
186 setPortProtocol(tmpPort
, cvServerData
.getAsInteger(Servers
.USE_SSL
));
188 setAuthType(cvServerData
.getAsInteger(Servers
.AUTH_TYPE
));
190 authRequired
= ( authType
!= Servers
.AUTH_NONE
);
191 username
= cvServerData
.getAsString(Servers
.USERNAME
);
192 password
= cvServerData
.getAsString(Servers
.PASSWORD
);
194 if ( !initialised
) initialise();
198 private void initialise() {
199 httpParams
= AcalConnectionPool
.defaultHttpParams(null, socketTimeOut
, connectionTimeOut
);
200 connManager
= AcalConnectionPool
.getHttpConnectionPool();
201 httpClient
= new DefaultHttpClient(connManager
, httpParams
);
207 * Takes the current AcalRequestor values and applies them to the Server ContentValues
208 * to be saved back in the database. Used during the server discovery process.
209 * @param cvServerData
211 public void applyToServerSettings(ContentValues cvServerData
) {
212 cvServerData
.put(Servers
.HOSTNAME
, hostName
);
213 cvServerData
.put(Servers
.USE_SSL
, (protocol
.equals("https")?
1:0));
214 cvServerData
.put(Servers
.PORT
, port
);
215 cvServerData
.put(Servers
.PRINCIPAL_PATH
, path
);
216 cvServerData
.put(Servers
.AUTH_TYPE
, authType
);
221 * Retrieve the HTTP headers received with the most recent response.
224 public Header
[] getResponseHeaders() {
225 return this.responseHeaders
;
229 * Retrieve the HTTP status code of the most recent response.
232 public int getStatusCode() {
233 return this.statusCode
;
237 * Interpret the URI in the string to set protocol, host, port & path for the next request.
238 * If the URI only matches a path part then protocol/host/port will be unchanged. This call
239 * will only allow for path parts that are anchored to the web root. This is generally used
240 * internally for following Location: redirects.
243 public void interpretUriString(String uriString
) {
245 // Match a URL, including an ipv6 address like http://[DEAD:BEEF:CAFE:F00D::]:8008/
246 final Pattern uriMatcher
= Pattern
.compile(
247 "^(?:(https?)://)?" + // Protocol
249 "(?:(?:[a-z0-9-]+[.]){2,7}(?:[a-z0-9-]+))" + // Hostname or IPv4 address
250 "|(?:\\[(?:[0-9a-f]{0,4}:)+(?:[0-9a-f]{0,4})?\\])" + // IPv6 address
252 "(?:[:]([0-9]{2,5}))?" + // Port number
253 "(/.*)?$" // Path bit.
254 ,Pattern
.CASE_INSENSITIVE
| Pattern
.DOTALL
);
256 final Pattern pathMatcher
= Pattern
.compile("^(/.*)$");
258 if ( Constants
.LOG_VERBOSE
) Log
.println(Constants
.LOGV
,TAG
,"Interpreting '"+uriString
+"'");
259 Matcher m
= uriMatcher
.matcher(uriString
);
261 if ( m
.group(1) != null && !m
.group(1).equals("") ) {
262 if ( Constants
.LOG_VERBOSE
) Log
.v(TAG
,"Found protocol '"+m
.group(1)+"'");
263 protocol
= m
.group(1);
264 if ( m
.group(3) == null || m
.group(3).equals("") ) {
265 port
= (protocol
.equals("http") ?
80 : 443);
268 if ( m
.group(2) != null ) {
269 if ( Constants
.LOG_VERBOSE
) Log
.v(TAG
,"Found hostname '"+m
.group(2)+"'");
270 setHostName( m
.group(2) );
272 if ( m
.group(3) != null && !m
.group(3).equals("") ) {
273 if ( Constants
.LOG_VERBOSE
) Log
.v(TAG
,"Found port '"+m
.group(3)+"'");
274 port
= Integer
.parseInt(m
.group(3));
275 if ( m
.group(1) != null && (port
== 0 || port
== 80 || port
== 443) ) {
276 port
= (protocol
.equals("http") ?
80 : 443);
279 if ( m
.group(4) != null && !m
.group(4).equals("") ) {
280 if ( Constants
.LOG_VERBOSE
) Log
.v(TAG
,"Found path '"+m
.group(4)+"'");
283 if ( !initialised
) initialise();
286 m
= pathMatcher
.matcher(uriString
);
288 if ( Constants
.LOG_VERBOSE
) Log
.v(TAG
,"Found relative path '"+m
.group(1)+"'");
289 setPath( m
.group(1) );
292 if ( Constants
.LOG_DEBUG
) Log
.d(TAG
, "Using Uri class to process redirect...");
293 Uri newLocation
= Uri
.parse(uriString
);
294 if ( newLocation
.getHost() != null ) setHostName( newLocation
.getHost() );
295 setPortProtocol( newLocation
.getPort(), newLocation
.getScheme());
296 setPath( newLocation
.getPath() );
297 if ( Constants
.LOG_VERBOSE
) Log
.v(TAG
,"Found new location at '"+fullUrl()+"'");
305 * When a request fails with a 401 Unauthorized you can call this with the content
306 * of the WWW-Authenticate header in the response and it will modify the URI so that
307 * if you repeat the request the correct authentication should be used.
309 * If you then get a 401, and this gets called again on that same Uri, it will throw
310 * an AuthenticationFailure exception rather than continue futilely.
312 * @param authRequestHeader
313 * @throws AuthenticationFailure
315 public void interpretRequestedAuth( Header authRequestHeader
) throws AuthenticationFailure
{
316 // Adjust our authentication setup so the next request will be able
317 // to send the correct authentication headers...
319 // 'WWW-Authenticate: Digest realm="DAViCal CalDAV Server", qop="auth", nonce="55a1a0c53c0f337e4675befabeff6a122b5b78de", opaque="52295deb26cc99c2dcc6614e70ed471f7a163e7a", algorithm="MD5"'
321 if ( Constants
.LOG_VERBOSE
)
322 Log
.v(TAG
,"Interpreting '"+authRequestHeader
+"'");
325 for( HeaderElement he
: authRequestHeader
.getElements() ) {
326 if ( Constants
.LOG_VERBOSE
)
327 Log
.v(TAG
,"Interpreting Element: '"+he
.toString()+"' ("+he
.getName()+":"+he
.getValue()+")");
329 if ( name
.length() > 6 && name
.substring(0, 7).equalsIgnoreCase("Digest ") ) {
330 authType
= Servers
.AUTH_DIGEST
;
331 name
= name
.substring(7);
333 else if ( name
.length() > 5 && name
.substring(0, 6).equalsIgnoreCase("Basic ") ) {
334 authType
= Servers
.AUTH_BASIC
;
335 name
= name
.substring(6);
338 if ( name
.equalsIgnoreCase("realm") ) {
339 authRealm
= he
.getValue();
340 if ( Constants
.LOG_VERBOSE
) Log
.v(TAG
,"Found '"+getAuthTypeName(authType
)+"' auth, realm: "+authRealm
);
342 else if ( name
.equalsIgnoreCase("qop") ) {
345 else if ( name
.equalsIgnoreCase("nonce") ) {
346 nonce
= he
.getValue();
348 else if ( name
.equalsIgnoreCase("opaque") ) {
349 opaque
= he
.getValue();
351 else if ( name
.equalsIgnoreCase("algorithm") ) {
361 private String
md5( String in
) {
363 MessageDigest digest
;
365 digest
= java
.security
.MessageDigest
.getInstance("MD5");
366 digest
.update(in
.getBytes());
367 return StaticHelpers
.toHexString(digest
.digest());
369 catch (NoSuchAlgorithmException e
) {
370 Log
.e(TAG
, e
.getMessage());
371 Log
.v(TAG
, Log
.getStackTraceString(e
));
377 private Header
buildAuthHeader() throws AuthenticationFailure
{
380 case Servers
.AUTH_BASIC
:
381 authValue
= String
.format("Basic %s", Base64Coder
.encodeString(username
+":"+password
));
382 if ( Constants
.LOG_VERBOSE
)
383 Log
.v(TAG
, "BasicAuthDebugging: '"+authValue
+"'" );
385 case Servers
.AUTH_DIGEST
:
386 String A1
= md5( username
+ ":" + authRealm
+ ":" + password
);
387 String A2
= md5( method
+ ":" + path
);
388 cnonce
= md5(userAgent
);
389 String printNC
= String
.format("%08x", ++authNC
);
390 String responseString
= A1
+":"+nonce
+":"+printNC
+":"+cnonce
+":auth:"+A2
;
391 if ( Constants
.LOG_VERBOSE
&& Constants
.debugDavCommunication
)
392 Log
.v(TAG
, "DigestDebugging: '"+responseString
+"'" );
393 String response
= md5(responseString
);
394 authValue
= String
.format("Digest realm=\"%s\", username=\"%s\", nonce=\"%s\", uri=\"%s\""
395 + ", response=\"%s\", algorithm=\"MD5\", cnonce=\"%s\", opaque=\"%s\", nc=\"%s\""
396 + (qop
== null ?
"" : ", qop=\"auth\""),
397 authRealm
, username
, nonce
, path
,
398 response
, cnonce
, opaque
, printNC
);
401 throw new AuthenticationFailure("Unknown authentication type");
403 return new BasicHeader("Authorization", authValue
);
408 * Get the current path used for the last request, or recently set.
411 public String
getPath() {
417 * Get the current authentication type used for the last request, or recently set.
420 public int getAuthType() {
426 * Set the port and protocol to the supplied values, with sanity checking.
427 * @param newPort As an integer. Numbers < 1 or > 65535 are ignored.
428 * @param newProtocol As an integer where 1 is https and anything else is http
430 public void setPortProtocol(Integer newPort
, Integer newProtocol
) {
431 protocol
= (newProtocol
== null || newProtocol
== 1 ?
"https" : "http");
432 if ( newPort
== null || newPort
< 1 || newPort
> 65535 || newPort
== 80 || newPort
== 443 )
433 port
= (protocol
.equals("http") ?
80 : 443);
440 * Set the port and protocol to the supplied values, with sanity checking. If the supplied
441 * newProtocol is null then we initially fall back to the current protocol, or http if that
443 * @param newPort As an integer. Numbers < 1 or > 65535 are ignored.
444 * @param newProtocol As a string like 'http' or 'https'
446 public void setPortProtocol(Integer newPort
, String newProtocol
) {
447 protocol
= (newProtocol
== null ? protocol
: (newProtocol
.equals("https") ?
"https" : "http"));
448 if ( newPort
== null || newPort
< 1 || newPort
> 65535 || newPort
== 80 || newPort
== 443 )
449 port
= (protocol
.equals("http") ?
80 : 443);
456 * Set the timeouts to use for subsequent requests, in milliseconds. The connectionTimeOut
457 * says how long to wait for the connection to be established, and the socketTimeOut says
458 * how long to wait for data after the connection is established.
459 * @param newConnectionTimeOut
460 * @param newSocketTimeOut
462 public void setTimeOuts( int newConnectionTimeOut
, int newSocketTimeOut
) {
463 if ( socketTimeOut
== newSocketTimeOut
&& connectionTimeOut
== newConnectionTimeOut
) return;
464 socketTimeOut
= newSocketTimeOut
;
465 connectionTimeOut
= newConnectionTimeOut
;
466 if ( !initialised
) return;
467 AcalConnectionPool
.setTimeOuts(socketTimeOut
,connectionTimeOut
);
468 httpClient
= new DefaultHttpClient(connManager
, httpParams
);
473 * Set the path for the next request, with some sanity checking to force the path
474 * to start with a '/'.
477 public void setPath(String newPath
) {
478 if ( newPath
== null || newPath
.equals("") ) {
482 if ( !newPath
.substring(0, 1).equals("/") ) {
483 path
= "/" + newPath
;
491 * Set the authentication type to be used for the next request.
494 public void setAuthType( Integer newAuthType
) {
495 if ( newAuthType
== Servers
.AUTH_BASIC
|| newAuthType
== Servers
.AUTH_DIGEST
) {
496 authType
= newAuthType
;
499 authType
= Servers
.AUTH_NONE
;
504 * Force the next request to use authentication pre-emptively.
506 public void setAuthRequired() {
512 * Return the current protocol://host:port as the start of a URL.
515 public String
protocolHostPort() {
519 + ((protocol
.equals("http") && port
== 80) || (protocol
.equals("https") && port
== 443) ?
"" : ":"+Integer
.toString(port
));
524 * Return the current protocol://host.example.com:port/path/to/resource as a URL.
527 public String
fullUrl() {
528 return protocolHostPort() + path
;
533 * Retrieve the unlocalised name of the authentication scheme currently in effect.
536 public static String
getAuthTypeName(int authCode
) {
538 // Only used in debug logging so don't need l10n
539 case Servers
.AUTH_BASIC
: return "Basic";
540 case Servers
.AUTH_DIGEST
: return "Digest";
541 default: return "NoAuth";
546 private String
getLocationHeader() {
547 for( Header h
: responseHeaders
) {
548 if (Constants
.LOG_VERBOSE
&& Constants
.debugDavCommunication
)
549 Log
.v(TAG
, "Looking for redirect in Header: " + h
.getName() + ":" + h
.getValue());
550 if (h
.getName().equalsIgnoreCase("Location"))
557 private Header
getAuthHeader() {
558 Header selectedAuthHeader
= null;
559 for( Header h
: responseHeaders
) {
560 if (Constants
.LOG_VERBOSE
&& Constants
.debugDavCommunication
)
561 Log
.v(TAG
, "Looking for auth in Header: " + h
.getName() + ":" + h
.getValue());
562 if ( h
.getName().equalsIgnoreCase("WWW-Authenticate") ) {
563 // If this is a digest Auth header we will return with it
564 for( HeaderElement he
: h
.getElements() ) {
566 if ( he
.getName().substring(0, 7).equalsIgnoreCase("Digest ") ) {
569 else if ( he
.getName().substring(0, 6).equalsIgnoreCase("Basic ") ) {
570 if ( selectedAuthHeader
== null ) selectedAuthHeader
= h
;
575 return selectedAuthHeader
;
581 private synchronized InputStream
sendRequest( Header
[] headers
, String entityString
)
582 throws SendRequestFailedException
, SSLException
, AuthenticationFailure
,
583 ConnectionFailedException
, ConnectionPoolTimeoutException
{
586 long start
= System
.currentTimeMillis();
588 if ( !initialised
) throw new IllegalStateException("AcalRequestor has not been initialised!");
591 // Create request and add headers and entity
592 DavRequest request
= new DavRequest(method
, this.fullUrl());
593 // request.addHeader(new BasicHeader("User-Agent", userAgent));
594 if ( headers
!= null ) for (Header h
: headers
) request
.addHeader(h
);
596 if ( authRequired
&& authType
!= Servers
.AUTH_NONE
)
597 request
.addHeader(buildAuthHeader());
599 if (entityString
!= null) {
600 request
.setEntity(new StringEntity(entityString
.toString(),"UTF-8"));
601 up
= request
.getEntity().getContentLength();
605 // This trick greatly reduces the occurrence of host not found errors.
606 try { InetAddress
.getByName(this.hostName
); } catch (UnknownHostException e1
) {
608 try { InetAddress
.getByName(this.hostName
); } catch (UnknownHostException e2
) {
613 int requestPort
= -1;
614 String requestProtocol
= this.protocol
;
615 if ( (this.protocol
.equals("http") && this.port
!= 80 )
616 || ( this.protocol
.equals("https") && this.port
!= 443 )
618 requestPort
= this.port
;
621 if ( Constants
.LOG_DEBUG
) {
622 Log
.d(TAG
, String
.format("Method: %s, Protocol: %s, Hostname: %s, Port: %d, Path: %s",
623 method
, requestProtocol
, hostName
, requestPort
, path
) );
625 HttpHost host
= new HttpHost(this.hostName
, requestPort
, requestProtocol
);
627 if ( Constants
.LOG_VERBOSE
&& Constants
.debugDavCommunication
) {
628 Log
.println(Constants
.LOGV
,TAG
, method
+" "+this.fullUrl());
630 for ( Header h
: request
.getAllHeaders() ) {
631 Log
.println(Constants
.LOGV
,TAG
,"H> "+h
.getName()+":"+h
.getValue() );
633 if (entityString
!= null) {
634 Log
.println(Constants
.LOGV
,TAG
, "----------------------- vvv Request Body vvv -----------------------" );
635 for( String line
: entityString
.toString().split("\n") ) {
636 if ( line
.length() == entityString
.toString().length() ) {
638 int length
= line
.length();
639 for( int pos
=0; pos
< length
; pos
+= LONG_LINE_WRAP_FOR_DEBUG
) {
640 end
= pos
+LONG_LINE_WRAP_FOR_DEBUG
;
641 if ( end
> length
) end
= length
;
642 Log
.println(Constants
.LOGV
,TAG
, "R> " + line
.substring(pos
, end
) );
646 Log
.println(Constants
.LOGV
,TAG
, "R> " + line
.replaceAll("\r$", "") );
649 Log
.println(Constants
.LOGV
,TAG
, "----------------------- ^^^ Request Body ^^^ -----------------------" );
654 // Send request and get response
655 HttpResponse response
= null;
658 if ( Constants
.debugHeap
) AcalDebug
.heapDebug(TAG
, "Making HTTP request");
659 response
= httpClient
.execute(host
,request
);
660 if ( Constants
.debugHeap
) AcalDebug
.heapDebug(TAG
, "Finished HTTP request");
662 catch (ConnectionPoolTimeoutException e
) {
663 Log
.i(TAG
, e
.getClass().getSimpleName() + ": " + e
.getMessage() + " to " + fullUrl() );
664 Log
.i(TAG
, "Retrying...");
665 response
= httpClient
.execute(host
,request
);
667 this.responseHeaders
= response
.getAllHeaders();
668 this.statusCode
= response
.getStatusLine().getStatusCode();
670 HttpEntity entity
= response
.getEntity();
671 down
= (entity
== null ?
0 : entity
.getContentLength());
673 long finish
= System
.currentTimeMillis();
674 double timeTaken
= ((double)(finish
-start
))/1000.0;
676 if ( Constants
.LOG_DEBUG
)
677 Log
.d(TAG
, "Response: "+statusCode
+", Sent: "+up
+", Received: "+down
+", Took: "+timeTaken
+" seconds");
679 if ( Constants
.LOG_VERBOSE
&& Constants
.debugDavCommunication
) {
680 for (Header h
: responseHeaders
) {
681 Log
.println(Constants
.LOGV
,TAG
,"H< "+h
.getName()+": "+h
.getValue() );
683 if (entity
!= null) {
684 if ( Constants
.LOG_DEBUG
&& Constants
.debugDavCommunication
) {
685 Log
.println(Constants
.LOGV
,TAG
, "----------------------- vvv Response Body vvv -----------------------" );
686 InputStream in
= entity
.getContent();
687 BufferedReader r
= new BufferedReader(new InputStreamReader(in
),AcalConnectionPool
.DEFAULT_BUFFER_SIZE
);
688 StringBuilder total
= new StringBuilder();
690 while ((line
= r
.readLine()) != null) {
694 int length
= line
.length();
695 for( int pos
=0; pos
< length
; pos
+= LONG_LINE_WRAP_FOR_DEBUG
) {
696 end
= pos
+LONG_LINE_WRAP_FOR_DEBUG
;
697 if ( end
> length
) end
= length
;
698 Log
.println(Constants
.LOGV
,TAG
, "R< " + line
.substring(pos
, end
).replaceAll("\r", "\\r") );
701 Log
.println(Constants
.LOGV
,TAG
, "----------------------- ^^^ Response Body ^^^ -----------------------" );
703 return new ByteArrayInputStream( total
.toString().getBytes() );
707 if (entity
!= null) {
708 if ( entity
.getContentLength() > 0 ) return entity
.getContent();
710 // Kind of admitting defeat here, but I can't track down why we seem
711 // to end up in never-never land if we just return entity.getContent()
712 // directly when entity.getContentLength() is -1 ('unknown', apparently).
713 // Horribly inefficient too.
715 // @todo: Check whether this problem was caused by failing to close the InputStream
716 // and this hack can be removed... Need to find a server which does not send Content-Length headers.
718 InputStream in
= entity
.getContent();
719 BufferedReader r
= new BufferedReader(new InputStreamReader(in
),AcalConnectionPool
.DEFAULT_BUFFER_SIZE
);
720 StringBuilder total
= new StringBuilder();
722 while ( (line
= r
.readLine()) != null ) {
723 total
.append(line
).append("\n");
726 return new ByteArrayInputStream( total
.toString().getBytes() );
730 catch (SSLException e
) {
731 if ( Constants
.LOG_DEBUG
&& Constants
.debugDavCommunication
)
732 Log
.d(TAG
,Log
.getStackTraceString(e
));
735 catch (AuthenticationFailure e
) {
736 if ( Constants
.LOG_DEBUG
&& Constants
.debugDavCommunication
)
737 Log
.d(TAG
,Log
.getStackTraceString(e
));
740 catch (SocketException e
) {
741 Log
.i(TAG
, e
.getClass().getSimpleName() + ": " + e
.getMessage() + " to " + fullUrl() );
743 catch (ConnectionPoolTimeoutException e
) {
744 Log
.i(TAG
, e
.getClass().getSimpleName() + ": " + e
.getMessage() + " to " + fullUrl() );
747 catch (ConnectTimeoutException e
) {
748 Log
.i(TAG
, e
.getClass().getSimpleName() + ": " + e
.getMessage() + " to " + fullUrl() );
749 throw new ConnectionFailedException( e
.getClass().getSimpleName() + ": " + fullUrl() );
751 catch ( UnknownHostException e
) {
752 Log
.i(TAG
, e
.getClass().getSimpleName() + ": " + e
.getMessage() + " to " + fullUrl() );
753 throw new ConnectionFailedException( e
.getClass().getSimpleName() + ": " + fullUrl() );
755 catch (Exception e
) {
756 Log
.d(TAG
,Log
.getStackTraceString(e
));
757 if ( statusCode
< 300 || statusCode
> 499 )
758 throw new SendRequestFailedException(e
.getMessage());
765 * Do a new HTTP <method> request with these headers and entity (request body) against
766 * this path (or the current path, if null). The headers & entity may also be null in
769 * If the server requests Digest or Basic authentication a second request will be made
770 * supplying these (if possible). Likewise the method will follow up to five redirects
771 * before giving up on a request.
777 * @throws SendRequestFailedException
778 * @throws SSLException
779 * @throws ConnectionFailedException
781 public InputStream
doRequest( String method
, String path
, Header
[] headers
, String entity
)
782 throws SendRequestFailedException
, SSLException
, ConnectionFailedException
{
783 InputStream result
= null;
784 this.method
= method
;
785 if ( path
!= null ) this.path
= path
;
787 if ( Constants
.LOG_DEBUG
)
788 Log
.d(TAG
, String
.format("%s request on %s", method
, fullUrl()) );
789 result
= sendRequest( headers
, entity
);
791 catch (SSLException e
) { throw e
; }
792 catch (SendRequestFailedException e
) { throw e
; }
793 catch (ConnectionFailedException e
) { throw e
; }
794 catch (AuthenticationFailure e1
) { statusCode
= 401; }
795 catch (Exception e
) {
796 Log
.e(TAG
,Log
.getStackTraceString(e
));
799 if ( statusCode
== 401 ) {
800 // In this case we didn't send auth credentials the first time, so
801 // we need to try again after we interpret the auth request.
803 interpretRequestedAuth(getAuthHeader());
804 return sendRequest( headers
, entity
);
806 catch (AuthenticationFailure e1
) {
807 throw new SendRequestFailedException("Authentication Failed: "+e1
.getMessage());
809 catch (Exception e
) {
810 Log
.e(TAG
,Log
.getStackTraceString(e
));
814 if ( (statusCode
>= 300 && statusCode
<= 303) || statusCode
== 307 ) {
816 * Other than 301/302 these are all pretty unlikely
817 * 300: Multiple choices, but we take the one in the Location header anyway
818 * 301: Moved permanently
819 * 302: Found (was 'temporary redirect' once in prehistory)
821 * 307: Temporary redirect. Meh.
823 String oldUrl
= fullUrl();
824 interpretUriString(getLocationHeader());
825 if (Constants
.LOG_DEBUG
)
826 Log
.d(TAG
, method
+ " " +oldUrl
+" redirected to: "+fullUrl());
827 if ( redirectCount
++ < redirectLimit
) {
829 return doRequest( method
, null, headers
, entity
);
839 * Does an XML request against the specified path (or the previously set path, if null),
840 * following redirects and returning the root DavNode of an XML tree.
844 * A DavNode which is the root of the multistatus response, or null if it couldn't be parsed.
847 public DavNode
doXmlRequest( String method
, String requestPath
, Header
[] headers
, String xml
) {
848 long start
= System
.currentTimeMillis();
850 if ( Constants
.LOG_DEBUG
)
851 Log
.d(TAG
, String
.format("%s XML request on %s", method
, fullUrl()) );
855 InputStream responseStream
= doRequest(method
, requestPath
, headers
, xml
);
856 if ( responseHeaders
== null ) {
857 responseStream
.close();
860 for( Header h
: responseHeaders
) {
861 if ( "Content-Type".equals(h
.getName()) ) {
862 for( HeaderElement he
: h
.getElements() ) {
863 if ( "text/plain".equals(he
.getName()) || "text/html".equals(he
.getName()) ) {
864 Log
.println(Constants
.LOGI
, TAG
, "Response is not an XML document");
865 responseStream
.close();
871 if ( statusCode
== 404 ) {
872 responseStream
.close();
875 root
= DavParserFactory
.buildTreeFromXml(Constants
.XMLParseMethod
, responseStream
);
877 catch (Exception e
) {
878 Log
.i(TAG
, e
.getMessage(), e
);
882 if (Constants
.LOG_VERBOSE
)
883 Log
.v(TAG
, "Request and parse completed in " + (System
.currentTimeMillis() - start
) + "ms");
888 * Get the current hostname used for the last request, or recently set.
891 public String
getHostName() {
892 return this.hostName
;
895 public void setHostName(String hostIn
) {
896 // This trick greatly reduces the occurrence of host not found errors.
897 try { InetAddress
.getByName(hostIn
); } catch (UnknownHostException e1
) { }
898 this.hostName
= hostIn
;
901 public int getPort() {
905 public String
getProtocol() {
909 public String
getUserName() {
910 return this.username
;