Don't wrap the lines quite as aggressively.
[acal.git] / src / com / morphoss / acal / service / connector / AcalRequestor.java
blob45e574d283ef2082e9484b269f3d56ccff2f1197
1 /*
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";
73 private int port = 0;
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.
114 * @param hostIn
115 * @param proto
116 * @param portIn
117 * @param pathIn
118 * @param user
119 * @param pass
121 public AcalRequestor( String hostIn, Integer proto, Integer portIn, String pathIn, String user, String pass ) {
122 setHostName(hostIn);
123 setPortProtocol(portIn,proto);
124 setPath(pathIn);
125 username = user;
126 password = pass;
128 initialise();
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
136 * @return
138 public static AcalRequestor fromSimpleValues( ContentValues cvServerData ) {
139 AcalRequestor result = new AcalRequestor();
141 result.protocol = "http";
142 result.hostName = null;
143 result.port = 0;
144 result.path = "/";
145 result.path = 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();
154 return result;
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
163 * @return
165 public static AcalRequestor fromServerValues( ContentValues cvServerData ) {
166 AcalRequestor result = new AcalRequestor();
167 result.applyFromServer(cvServerData);
168 return result;
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);
184 int tmpPort = 0;
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);
203 initialised = true;
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.
222 * @return
224 public Header[] getResponseHeaders() {
225 return this.responseHeaders;
229 * Retrieve the HTTP status code of the most recent response.
230 * @return
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.
241 * @param uriString
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
248 "(" + // host spec
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
251 ")" +
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);
260 if ( m.matches() ) {
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)+"'");
281 setPath(m.group(4));
283 if ( !initialised ) initialise();
285 else {
286 m = pathMatcher.matcher(uriString);
287 if (m.find()) {
288 if ( Constants.LOG_VERBOSE ) Log.v(TAG,"Found relative path '"+m.group(1)+"'");
289 setPath( m.group(1) );
291 else {
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+"'");
324 String name;
325 for( HeaderElement he : authRequestHeader.getElements() ) {
326 if ( Constants.LOG_VERBOSE )
327 Log.v(TAG,"Interpreting Element: '"+he.toString()+"' ("+he.getName()+":"+he.getValue()+")");
328 name = he.getName();
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") ) {
343 qop = "auth";
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") ) {
352 algorithm = "MD5";
357 authRequired = true;
361 private String md5( String in ) {
362 // Create MD5 Hash
363 MessageDigest digest;
364 try {
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));
373 return "";
377 private Header buildAuthHeader() throws AuthenticationFailure {
378 String authValue;
379 switch( authType ) {
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+"'" );
384 break;
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 );
399 break;
400 default:
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.
409 * @return
411 public String getPath() {
412 return path;
417 * Get the current authentication type used for the last request, or recently set.
418 * @return
420 public int getAuthType() {
421 return authType;
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);
434 else
435 port = newPort;
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
442 * is null.
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);
450 else
451 port = newPort;
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 '/'.
475 * @param newPath
477 public void setPath(String newPath) {
478 if ( newPath == null || newPath.equals("") ) {
479 path = "/";
480 return;
482 if ( !newPath.substring(0, 1).equals("/") ) {
483 path = "/" + newPath;
485 else
486 path = newPath;
491 * Set the authentication type to be used for the next request.
492 * @param newAuthType
494 public void setAuthType( Integer newAuthType ) {
495 if ( newAuthType == Servers.AUTH_BASIC || newAuthType == Servers.AUTH_DIGEST ) {
496 authType = newAuthType;
497 return;
499 authType = Servers.AUTH_NONE;
504 * Force the next request to use authentication pre-emptively.
506 public void setAuthRequired() {
507 authRequired = true;
512 * Return the current protocol://host:port as the start of a URL.
513 * @return
515 public String protocolHostPort() {
516 return protocol
517 + "://"
518 + hostName
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.
525 * @return
527 public String fullUrl() {
528 return protocolHostPort() + path;
533 * Retrieve the unlocalised name of the authentication scheme currently in effect.
534 * @return
536 public static String getAuthTypeName(int authCode) {
537 switch (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"))
551 return h.getValue();
553 return "";
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 ") ) {
567 return h;
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 {
584 long down = 0;
585 long up = 0;
586 long start = System.currentTimeMillis();
588 if ( !initialised ) throw new IllegalStateException("AcalRequestor has not been initialised!");
589 statusCode = -1;
590 try {
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) {
607 Thread.sleep(100);
608 try { InetAddress.getByName(this.hostName); } catch (UnknownHostException e2) {
609 Thread.sleep(100);
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() ) {
637 int end;
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) );
645 else {
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;
657 try {
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();
689 String line;
690 while ((line = r.readLine()) != null) {
691 total.append(line);
692 total.append("\n");
693 int end;
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 ^^^ -----------------------" );
702 in.close();
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();
721 String line;
722 while ( (line = r.readLine()) != null ) {
723 total.append(line).append("\n");
725 in.close();
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));
733 throw e;
735 catch (AuthenticationFailure e) {
736 if ( Constants.LOG_DEBUG && Constants.debugDavCommunication )
737 Log.d(TAG,Log.getStackTraceString(e));
738 throw 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() );
745 throw e;
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());
760 return null;
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
767 * some simple cases.
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.
772 * @param method
773 * @param path
774 * @param headers
775 * @param entity
776 * @return
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;
786 try {
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.
802 try {
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)
820 * 303: See other
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 ) {
828 // Follow redirect
829 return doRequest( method, null, headers, entity );
833 return result;
838 * <p>
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.
841 * </p>
843 * @return <p>
844 * A DavNode which is the root of the multistatus response, or null if it couldn't be parsed.
845 * </p>
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()) );
853 DavNode root = null;
854 try {
855 InputStream responseStream = doRequest(method, requestPath, headers, xml);
856 if ( responseHeaders == null ) {
857 responseStream.close();
858 return root;
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();
866 return root;
871 if ( statusCode == 404 ) {
872 responseStream.close();
873 return root;
875 root = DavParserFactory.buildTreeFromXml(Constants.XMLParseMethod, responseStream );
877 catch (Exception e) {
878 Log.i(TAG, e.getMessage(), e);
879 return null;
882 if (Constants.LOG_VERBOSE)
883 Log.v(TAG, "Request and parse completed in " + (System.currentTimeMillis() - start) + "ms");
884 return root;
888 * Get the current hostname used for the last request, or recently set.
889 * @return
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() {
902 return port;
905 public String getProtocol() {
906 return protocol;
909 public String getUserName() {
910 return this.username;