Refactoring

* Contacts/Event.toEntity() return ByteArrayOutputStream instead of String
* throw DavNoContentException instead of returning null to avoid NullPointerExceptions
* always close input/output streams
* always call consumeContent() for HTTP response entities to avoid memory leaks
pull/2/head
rfc2822 11 years ago
parent f4bb3639e6
commit 64068f7007

@ -11,6 +11,7 @@ public class LoggingInputStream extends FilterInputStream {
private static final int MAX_LENGTH = 10*1024; // don't log more than 10 kB of data
String tag;
InputStream proxy;
ByteArrayOutputStream log = new ByteArrayOutputStream(MAX_LENGTH);
int logSize = 0;
@ -20,6 +21,7 @@ public class LoggingInputStream extends FilterInputStream {
public LoggingInputStream(String tag, InputStream proxy) {
super(proxy);
this.tag = tag;
this.proxy = proxy;
}

@ -7,6 +7,7 @@
******************************************************************************/
package at.bitfire.davdroid.resource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
@ -183,7 +184,7 @@ public class Contact extends Resource {
@Override
public String toEntity() throws IOException {
public ByteArrayOutputStream toEntity() throws IOException {
VCard vcard = new VCard();
vcard.setProdId("DAVdroid/" + Constants.APP_VERSION + " (ez-vcard/" + Ezvcard.VERSION + ")");
@ -257,10 +258,13 @@ public class Contact extends Resource {
vcard.setBirthday(birthDay);
vcard.setRevision(Revision.now());
return Ezvcard
ByteArrayOutputStream os = new ByteArrayOutputStream();
Ezvcard
.write(vcard)
.version(VCardVersion.V3_0)
.prodId(false) // we provide or own PRODID
.go();
.go(os);
return os;
}
}

@ -7,6 +7,7 @@
******************************************************************************/
package at.bitfire.davdroid.resource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
@ -20,6 +21,7 @@ import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.data.CalendarOutputter;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.model.Component;
import net.fortuna.ical4j.model.ComponentList;
@ -29,6 +31,7 @@ import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory;
import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.PropertyList;
import net.fortuna.ical4j.model.TimeZoneRegistry;
import net.fortuna.ical4j.model.ValidationException;
import net.fortuna.ical4j.model.component.VAlarm;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VTimeZone;
@ -166,7 +169,7 @@ public class Event extends Resource {
@Override
@SuppressWarnings("unchecked")
public String toEntity() {
public ByteArrayOutputStream toEntity() throws IOException, ValidationException {
net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar();
ical.getProperties().add(Version.VERSION_2_0);
ical.getProperties().add(new ProdId("-//bitfire web engineering//DAVdroid " + Constants.APP_VERSION + "//EN"));
@ -224,8 +227,11 @@ public class Event extends Resource {
ical.getComponents().add(tzStart.getVTimeZone());
if (tzEnd != null && tzEnd != tzStart)
ical.getComponents().add(tzEnd.getVTimeZone());
return ical.toString();
CalendarOutputter output = new CalendarOutputter(false);
ByteArrayOutputStream os = new ByteArrayOutputStream();
output.output(ical, os);
return os;
}

@ -7,6 +7,8 @@
******************************************************************************/
package at.bitfire.davdroid.resource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
@ -19,12 +21,14 @@ import lombok.Getter;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.model.ValidationException;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpException;
import org.apache.http.protocol.HTTP;
import android.util.Log;
import at.bitfire.davdroid.LoggingInputStream;
import at.bitfire.davdroid.webdav.DAVException;
import at.bitfire.davdroid.webdav.DavException;
import at.bitfire.davdroid.webdav.DavMultiget;
import at.bitfire.davdroid.webdav.DavNoContentException;
import at.bitfire.davdroid.webdav.HttpPropfind;
import at.bitfire.davdroid.webdav.WebDavResource;
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
@ -50,13 +54,13 @@ public abstract class RemoteCollection<T extends Resource> {
try {
if (collection.getCTag() == null && collection.getMembers() == null) // not already fetched
collection.propfind(HttpPropfind.Mode.COLLECTION_CTAG);
} catch (DAVException e) {
} catch (DavException e) {
return null;
}
return collection.getCTag();
}
public Resource[] getMemberETags() throws IOException, DAVException, HttpException {
public Resource[] getMemberETags() throws IOException, DavException, HttpException {
collection.propfind(HttpPropfind.Mode.MEMBERS_ETAG);
List<T> resources = new LinkedList<T>();
@ -69,38 +73,35 @@ public abstract class RemoteCollection<T extends Resource> {
}
@SuppressWarnings("unchecked")
public Resource[] multiGet(Resource[] resources) throws IOException, DAVException, HttpException {
public Resource[] multiGet(Resource[] resources) throws IOException, DavException, HttpException {
try {
if (resources.length == 1) {
Resource resource = get(resources[0]);
return (resource != null) ? (T[]) new Resource[] { resource } : null;
}
if (resources.length == 1)
return (T[]) new Resource[] { get(resources[0]) };
LinkedList<String> names = new LinkedList<String>();
for (Resource resource : resources)
names.add(resource.getName());
collection.multiGet(multiGetType(), names.toArray(new String[0]));
if (collection.getMembers() == null)
throw new DavNoContentException();
LinkedList<T> foundResources = new LinkedList<T>();
if (collection.getMembers() != null)
for (WebDavResource member : collection.getMembers()) {
T resource = newResourceSkeleton(member.getName(), member.getETag());
try {
InputStream is = member.getContent();
if (is != null) {
resource.parseEntity(is);
foundResources.add(resource);
} else
Log.e(TAG, "Ignoring entity without content");
} catch (ParserException ex) {
Log.e(TAG, "Ignoring unparseable iCal in multi-response", ex);
} catch (VCardException ex) {
Log.e(TAG, "Ignoring unparseable vCard in multi-response", ex);
}
for (WebDavResource member : collection.getMembers()) {
T resource = newResourceSkeleton(member.getName(), member.getETag());
try {
if (member.getContent() != null) {
@Cleanup InputStream is = new ByteArrayInputStream(member.getContent());
resource.parseEntity(is);
foundResources.add(resource);
} else
Log.e(TAG, "Ignoring entity without content");
} catch (ParserException ex) {
Log.e(TAG, "Ignoring unparseable iCal in multi-response", ex);
} catch (VCardException ex) {
Log.e(TAG, "Ignoring unparseable vCard in multi-response", ex);
}
else
return null;
}
return foundResources.toArray(new Resource[0]);
} catch (ParserException ex) {
@ -115,19 +116,28 @@ public abstract class RemoteCollection<T extends Resource> {
/* internal member operations */
public Resource get(Resource resources) throws IOException, HttpException, ParserException, VCardException {
WebDavResource member = new WebDavResource(collection, resources.getName());
public Resource get(Resource resource) throws IOException, HttpException, ParserException, VCardException {
WebDavResource member = new WebDavResource(collection, resource.getName());
member.get();
@Cleanup InputStream loggedContent = new LoggingInputStream(TAG, member.getContent());
resources.parseEntity(loggedContent);
return resources;
byte[] data = member.getContent();
if (data == null)
throw new DavNoContentException();
Log.i(TAG, "Received content:");
Log.i(TAG, IOUtils.toString(data, HTTP.UTF_8));
@Cleanup InputStream is = new ByteArrayInputStream(data);
resource.parseEntity(is);
return resource;
}
public void add(Resource res) throws IOException, HttpException, ValidationException {
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
member.setContentType(memberContentType());
member.put(res.toEntity().getBytes("UTF-8"), PutMode.ADD_DONT_OVERWRITE);
@Cleanup ByteArrayOutputStream os = res.toEntity();
member.put(os.toByteArray(), PutMode.ADD_DONT_OVERWRITE);
collection.invalidateCTag();
}
@ -142,7 +152,9 @@ public abstract class RemoteCollection<T extends Resource> {
public void update(Resource res) throws IOException, HttpException, ValidationException {
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
member.setContentType(memberContentType());
member.put(res.toEntity().getBytes("UTF-8"), PutMode.UPDATE_DONT_OVERWRITE);
@Cleanup ByteArrayOutputStream os = res.toEntity();
member.put(os.toByteArray(), PutMode.UPDATE_DONT_OVERWRITE);
collection.invalidateCTag();
}

@ -7,6 +7,7 @@
******************************************************************************/
package at.bitfire.davdroid.resource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -38,5 +39,5 @@ public abstract class Resource {
public abstract void initialize();
public abstract void parseEntity(InputStream entity) throws IOException, ParserException, VCardException;
public abstract String toEntity() throws IOException, ValidationException;
public abstract ByteArrayOutputStream toEntity() throws IOException, ValidationException;
}

@ -22,7 +22,7 @@ import android.util.Log;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.LocalStorageException;
import at.bitfire.davdroid.resource.RemoteCollection;
import at.bitfire.davdroid.webdav.DAVException;
import at.bitfire.davdroid.webdav.DavException;
public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter {
private final static String TAG = "davdroid.DavSyncAdapter";
@ -71,7 +71,7 @@ public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter {
} catch (AuthenticationException ex) {
syncResult.stats.numAuthExceptions++;
Log.e(TAG, "HTTP authentication failed", ex);
} catch (DAVException ex) {
} catch (DavException ex) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, "Invalid DAV response", ex);
} catch (HttpException ex) {

@ -28,8 +28,8 @@ import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.Toast;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.IncapableResourceException;
import at.bitfire.davdroid.webdav.HttpPropfind.Mode;
import at.bitfire.davdroid.webdav.DavIncapableException;
import at.bitfire.davdroid.webdav.WebDavResource;
public class QueryServerDialogFragment extends DialogFragment implements LoaderCallbacks<ServerInfo> {
@ -117,7 +117,7 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
serverInfo.setCalDAV(base.supportsDAV("calendar-access"));
if (!base.supportsMethod("PROPFIND") || !base.supportsMethod("REPORT") ||
(!serverInfo.isCalDAV() && !serverInfo.isCardDAV()))
throw new IncapableResourceException(getContext().getString(R.string.neither_caldav_nor_carddav));
throw new DavIncapableException(getContext().getString(R.string.neither_caldav_nor_carddav));
// (2/5) get principal URL
base.propfind(Mode.CURRENT_USER_PRINCIPAL);
@ -126,7 +126,7 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
if (principalPath != null)
Log.i(TAG, "Found principal path: " + principalPath);
else
throw new IncapableResourceException(getContext().getString(R.string.error_principal_path));
throw new DavIncapableException(getContext().getString(R.string.error_principal_path));
// (3/5) get home sets
WebDavResource principal = new WebDavResource(base, principalPath);
@ -138,7 +138,7 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
if (pathAddressBooks != null)
Log.i(TAG, "Found address book home set: " + pathAddressBooks);
else
throw new IncapableResourceException(getContext().getString(R.string.error_home_set_address_books));
throw new DavIncapableException(getContext().getString(R.string.error_home_set_address_books));
}
String pathCalendars = null;
@ -147,7 +147,7 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
if (pathCalendars != null)
Log.i(TAG, "Found calendar home set: " + pathCalendars);
else
throw new IncapableResourceException(getContext().getString(R.string.error_home_set_calendars));
throw new DavIncapableException(getContext().getString(R.string.error_home_set_calendars));
}
// (4/5) get address books
@ -208,10 +208,10 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
serverInfo.setErrorMessage(getContext().getString(R.string.exception_uri_syntax, e.getMessage()));
} catch (IOException e) {
serverInfo.setErrorMessage(getContext().getString(R.string.exception_io, e.getLocalizedMessage()));
} catch (DavIncapableException e) {
serverInfo.setErrorMessage(getContext().getString(R.string.exception_incapable_resource, e.getLocalizedMessage()));
} catch (HttpException e) {
serverInfo.setErrorMessage(getContext().getString(R.string.exception_http, e.getLocalizedMessage()));
} catch (IncapableResourceException e) {
serverInfo.setErrorMessage(getContext().getString(R.string.exception_incapable_resource, e.getLocalizedMessage()));
}
return serverInfo;

@ -2,17 +2,17 @@ package at.bitfire.davdroid.webdav;
import org.apache.http.HttpException;
public class DAVException extends HttpException {
public class DavException extends HttpException {
private static final long serialVersionUID = -2118919144443165706L;
final private static String prefix = "Invalid DAV response: ";
public DAVException(String message) {
public DavException(String message) {
super(prefix + message);
}
public DAVException(String message, Throwable ex) {
public DavException(String message, Throwable ex) {
super(prefix + message, ex);
}
}

@ -5,15 +5,12 @@
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
******************************************************************************/
package at.bitfire.davdroid.resource;
package at.bitfire.davdroid.webdav;
public class IncapableResourceException extends Exception {
public class DavIncapableException extends DavException {
private static final long serialVersionUID = -7199786680939975667L;
public IncapableResourceException() {
}
public IncapableResourceException(String msg) {
public DavIncapableException(String msg) {
super(msg);
}
}

@ -1,11 +1,11 @@
package at.bitfire.davdroid.webdav;
public class DAVNoContentException extends DAVException {
public class DavNoContentException extends DavException {
private static final long serialVersionUID = 6256645020350945477L;
private final static String message = "HTTP response entity (content) expected but not received";
public DAVNoContentException() {
public DavNoContentException() {
super(message);
}
}

@ -1,11 +1,11 @@
package at.bitfire.davdroid.webdav;
public class DAVNoMultiStatusException extends DAVException {
public class DavNoMultiStatusException extends DavException {
private static final long serialVersionUID = -3600405724694229828L;
private final static String message = "207 Multi-Status expected but not received";
public DAVNoMultiStatusException() {
public DavNoMultiStatusException() {
super(message);
}
}

@ -7,7 +7,6 @@
******************************************************************************/
package at.bitfire.davdroid.webdav;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
@ -24,6 +23,7 @@ import lombok.Cleanup;
import lombok.Getter;
import lombok.ToString;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
@ -41,6 +41,7 @@ import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicLineParser;
import org.apache.http.protocol.HTTP;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
@ -84,7 +85,7 @@ public class WebDavResource {
@Getter protected List<WebDavResource> members;
// content (available after GET)
@Getter protected InputStream content;
@Getter protected byte[] content;
protected DefaultHttpClient client;
@ -129,32 +130,6 @@ public class WebDavResource {
this(parent, member);
properties.put(Property.ETAG, ETag);
}
protected void checkResponse(HttpResponse response) throws HttpException {
checkResponse(response.getStatusLine());
}
protected void checkResponse(StatusLine statusLine) throws HttpException {
int code = statusLine.getStatusCode();
Log.d(TAG, "Received " + statusLine.getProtocolVersion() + " " + code + " " + statusLine.getReasonPhrase());
if (code/100 == 1 || code/100 == 2) // everything OK
return;
String reason = code + " " + statusLine.getReasonPhrase();
switch (code) {
case HttpStatus.SC_UNAUTHORIZED:
throw new AuthenticationException(reason);
case HttpStatus.SC_NOT_FOUND:
throw new NotFoundException(reason);
case HttpStatus.SC_PRECONDITION_FAILED:
throw new PreconditionFailedException(reason);
default:
throw new HttpException(reason);
}
}
/* feature detection */
@ -163,7 +138,10 @@ public class WebDavResource {
HttpOptions options = new HttpOptions(location);
HttpResponse response = client.execute(options);
checkResponse(response);
if (response.getEntity() != null)
response.getEntity().consumeContent();
Header[] allowHeaders = response.getHeaders("Allow");
for (Header allowHeader : allowHeaders)
methods.addAll(Arrays.asList(allowHeader.getValue().split(", ?")));
@ -250,34 +228,34 @@ public class WebDavResource {
/* collection operations */
public void propfind(HttpPropfind.Mode mode) throws IOException, DAVException, HttpException {
public void propfind(HttpPropfind.Mode mode) throws IOException, DavException, HttpException {
HttpPropfind propfind = new HttpPropfind(location, mode);
HttpResponse response = client.execute(propfind);
checkResponse(response);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS)
throw new DAVNoMultiStatusException();
throw new DavNoMultiStatusException();
@Cleanup("consumeContent") HttpEntity entity = response.getEntity();
if (entity == null)
throw new DAVNoContentException();
InputStream content = entity.getContent();
if (content == null)
throw new DAVNoContentException();
throw new DavNoContentException();
@Cleanup LoggingInputStream loggedContent = new LoggingInputStream(TAG, content);
InputStream rawContent = entity.getContent();
if (content == null)
throw new DavNoContentException();
@Cleanup LoggingInputStream content = new LoggingInputStream(TAG, rawContent);
DavMultistatus multistatus;
try {
Serializer serializer = new Persister();
multistatus = serializer.read(DavMultistatus.class, loggedContent, false);
multistatus = serializer.read(DavMultistatus.class, content, false);
} catch (Exception ex) {
throw new DAVException("Couldn't parse Multi-Status response on PROPFIND", ex);
throw new DavException("Couldn't parse Multi-Status response on PROPFIND", ex);
}
processMultiStatus(multistatus);
}
public void multiGet(DavMultiget.Type type, String[] names) throws IOException, DAVException, HttpException {
public void multiGet(DavMultiget.Type type, String[] names) throws IOException, DavException, HttpException {
List<String> hrefs = new LinkedList<String>();
for (String name : names)
hrefs.add(location.resolve(name).getRawPath());
@ -289,7 +267,7 @@ public class WebDavResource {
serializer.write(multiget, writer);
} catch (Exception ex) {
Log.e(TAG, "Couldn't create XML multi-get request", ex);
throw new DAVException("Couldn't create multi-get request");
throw new DavException("Couldn't create multi-get request");
}
HttpReport report = new HttpReport(location, writer.toString());
@ -297,22 +275,22 @@ public class WebDavResource {
checkResponse(response);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS)
throw new DAVNoMultiStatusException();
throw new DavNoMultiStatusException();
@Cleanup("consumeContent") HttpEntity entity = response.getEntity();
if (entity == null)
throw new DAVNoContentException();
InputStream content = entity.getContent();
if (content == null)
throw new DAVNoContentException();
throw new DavNoContentException();
@Cleanup LoggingInputStream loggedContent = new LoggingInputStream(TAG, content);
InputStream rawContent = entity.getContent();
if (rawContent == null)
throw new DavNoContentException();
@Cleanup LoggingInputStream content = new LoggingInputStream(TAG, rawContent);
DavMultistatus multiStatus;
try {
multiStatus = serializer.read(DavMultistatus.class, loggedContent, false);
multiStatus = serializer.read(DavMultistatus.class, content, false);
} catch (Exception ex) {
throw new DAVException("Couldn't parse Multi-Status response on REPORT multi-get", ex);
throw new DavException("Couldn't parse Multi-Status response on REPORT multi-get", ex);
}
processMultiStatus(multiStatus);
}
@ -325,12 +303,17 @@ public class WebDavResource {
HttpResponse response = client.execute(get);
checkResponse(response);
content = response.getEntity().getContent();
@Cleanup("consumeContent") HttpEntity entity = response.getEntity();
if (entity.getContent() == null)
throw new DavNoContentException();
content = IOUtils.toByteArray(entity.getContent());
}
public void put(byte[] data, PutMode mode) throws IOException, HttpException {
Log.d(TAG, "Sending PUT request:");
Log.d(TAG, IOUtils.toString(data, HTTP.UTF_8));
HttpPut put = new HttpPut(location);
Log.d(TAG, "Sending PUT request: " + new String(data, "UTF-8"));
put.setEntity(new ByteArrayEntity(data));
switch (mode) {
@ -360,9 +343,34 @@ public class WebDavResource {
/* helpers */
protected void checkResponse(HttpResponse response) throws HttpException {
checkResponse(response.getStatusLine());
}
protected void checkResponse(StatusLine statusLine) throws HttpException {
int code = statusLine.getStatusCode();
Log.d(TAG, "Received " + statusLine.getProtocolVersion() + " " + code + " " + statusLine.getReasonPhrase());
if (code/100 == 1 || code/100 == 2) // everything OK
return;
String reason = code + " " + statusLine.getReasonPhrase();
switch (code) {
case HttpStatus.SC_UNAUTHORIZED:
throw new AuthenticationException(reason);
case HttpStatus.SC_NOT_FOUND:
throw new NotFoundException(reason);
case HttpStatus.SC_PRECONDITION_FAILED:
throw new PreconditionFailedException(reason);
default:
throw new HttpException(reason);
}
}
protected void processMultiStatus(DavMultistatus multistatus) throws HttpException {
if (multistatus.response == null) // empty response
return;
throw new DavNoContentException();
// member list will be built from response
List<WebDavResource> members = new LinkedList<WebDavResource>();
@ -443,9 +451,9 @@ public class WebDavResource {
properties.put(Property.ETAG, prop.getetag.getETag());
if (prop.calendarData != null && prop.calendarData.ical != null)
referenced.content = new ByteArrayInputStream(prop.calendarData.ical.getBytes());
referenced.content = prop.calendarData.ical.getBytes();
else if (prop.addressData != null && prop.addressData.vcard != null)
referenced.content = new ByteArrayInputStream(prop.addressData.vcard.getBytes());
referenced.content = prop.addressData.vcard.getBytes();
}
}

Loading…
Cancel
Save