Commit 8258787d authored by Ricki Hirner's avatar Ricki Hirner

Better ETag handling

* improved ETag parsing (including W/ prefix)
* in PUT/DELETE requests
* handle 412 Precondition failed
* StringUtils: quoted-string encoding/decoding
parent 487f8d54
/*
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.dav4android;
import junit.framework.TestCase;
public class StringUtilsTest extends TestCase {
public void testAsQuotedString() {
assertEquals(null, StringUtils.asQuotedString(null));
assertEquals("\"\"", StringUtils.asQuotedString(""));
assertEquals("\"\\\"\"", StringUtils.asQuotedString("\""));
assertEquals("\"\\\\\"", StringUtils.asQuotedString("\\"));
}
public void testDecodeQuotedString() {
assertEquals(null, StringUtils.decodeQuotedString(null));
assertEquals("\"", StringUtils.decodeQuotedString("\""));
assertEquals("\\", StringUtils.decodeQuotedString("\"\\\""));
assertEquals("\"test", StringUtils.decodeQuotedString("\"test"));
assertEquals("test", StringUtils.decodeQuotedString("test"));
assertEquals("", StringUtils.decodeQuotedString("\"\""));
assertEquals("test", StringUtils.decodeQuotedString("\"test\""));
assertEquals("test\\", StringUtils.decodeQuotedString("\"test\\\""));
assertEquals("test", StringUtils.decodeQuotedString("\"t\\e\\st\""));
assertEquals("12\"34", StringUtils.decodeQuotedString("\"12\\\"34\""));
assertEquals("1234\"", StringUtils.decodeQuotedString("\"1234\\\"\""));
}
}
......@@ -36,6 +36,7 @@ import java.util.Set;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.exception.InvalidDavResponseException;
import at.bitfire.dav4android.exception.PreconditionFailedException;
import at.bitfire.dav4android.exception.UnsupportedDavException;
import at.bitfire.dav4android.property.GetETag;
import at.bitfire.dav4android.property.ResourceType;
......@@ -63,7 +64,7 @@ public class DavResource {
public String fileName() {
List<String> pathSegments = location.pathSegments();
return pathSegments.get(pathSegments.size()-1);
return pathSegments.get(pathSegments.size() - 1);
}
@Override
......@@ -94,14 +95,18 @@ public class DavResource {
return body;
}
public void put(@NonNull RequestBody body, String ifMatchETag, String ifNoneMatchETag) throws IOException, HttpException {
public void put(@NonNull RequestBody body, String ifMatchETag, boolean ifNoneMatch) throws IOException, HttpException {
Request.Builder builder = new Request.Builder()
.put(body)
.url(location);
if (ifMatchETag != null)
builder.header("If-Match", ifMatchETag);
if (ifNoneMatchETag != null)
builder.header("If-None-Match", ifNoneMatchETag);
// only overwrite specific version
builder.header("If-Match", StringUtils.asQuotedString(ifMatchETag));
if (ifNoneMatch)
// don't overwrite anything existing
builder.header("If-None-Match", "*");
Response response = httpClient.newCall(builder.build()).execute();
checkStatus(response);
......@@ -112,12 +117,13 @@ public class DavResource {
properties.put(GetETag.NAME, new GetETag(eTag));
}
public void delete(@NonNull String ifMatchETag) throws IOException, HttpException {
Response response = httpClient.newCall(new Request.Builder()
public void delete(String ifMatchETag) throws IOException, HttpException {
Request.Builder builder = new Request.Builder()
.delete()
.url(location)
.header("If-Match", ifMatchETag)
.build()).execute();
.url(location);
if (ifMatchETag != null)
builder.header("If-Match", StringUtils.asQuotedString(ifMatchETag));
Response response = httpClient.newCall(builder.build()).execute();
checkStatus(response);
}
......@@ -181,7 +187,12 @@ public class DavResource {
// everything OK
return;
throw new HttpException(code, message);
switch (code) {
case 412:
throw new PreconditionFailedException(message);
default:
throw new HttpException(code, message);
}
}
protected void checkStatus(Response response) throws HttpException {
......
/*
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.dav4android;
public class StringUtils {
public static String asQuotedString(String raw) {
if (raw == null)
return null;
return "\"" + raw.replace("\\" ,"\\\\").replace("\"", "\\\"") + "\"";
}
public static String decodeQuotedString(String quoted) {
if (quoted == null)
return null;
/* quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
qdtext = <any TEXT except <">>
quoted-pair = "\" CHAR
*/
int len = quoted.length();
if (len >= 2 && quoted.charAt(0) == '"' && quoted.charAt(len-1) == '"') {
StringBuffer result = new StringBuffer(len);
//quoted = quoted.substring(1, len-1);
for (int pos = 1; pos < len-1; pos++) {
char c = quoted.charAt(pos);
if (c == '\\' && pos != len-2)
c = quoted.charAt(++pos);
result.append(c);
}
return result.toString();
} else
return quoted;
}
}
/*
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.dav4android.exception;
public class PreconditionFailedException extends HttpException {
public PreconditionFailedException(String message) {
super(412, message);
}
}
......@@ -52,7 +52,7 @@ public class AddressbookDescription implements Property {
eventType = parser.next();
}
} catch(XmlPullParserException|IOException e) {
Log.e(Constants.LOG_TAG, "Couldn't parse <addressbook-description>", e);
Constants.log.error("Couldn't parse <addressbook-description>", e);
return null;
}
......
......@@ -56,7 +56,7 @@ public class AddressbookHomeSet implements Property {
eventType = parser.next();
}
} catch(XmlPullParserException|IOException e) {
Log.e(Constants.LOG_TAG, "Couldn't parse <addressbook-home-set>", e);
Constants.log.error("Couldn't parse <addressbook-home-set>", e);
return null;
}
......
......@@ -8,6 +8,7 @@
package at.bitfire.dav4android.property;
import android.text.TextUtils;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
......@@ -18,6 +19,7 @@ import java.io.IOException;
import at.bitfire.dav4android.Constants;
import at.bitfire.dav4android.Property;
import at.bitfire.dav4android.PropertyFactory;
import at.bitfire.dav4android.StringUtils;
import at.bitfire.dav4android.XmlUtils;
import lombok.ToString;
......@@ -29,8 +31,19 @@ public class GetETag implements Property {
private GetETag() {}
public GetETag(String eTag) {
this.eTag = eTag;
public GetETag(String rawETag)
{
/* entity-tag = [ weak ] opaque-tag
weak = "W/"
opaque-tag = quoted-string
*/
// remove trailing "W/"
if (rawETag.startsWith("W/") && rawETag.length() >= 3)
// entity tag is weak (doesn't matter for us)
rawETag = rawETag.substring(2);
eTag = StringUtils.decodeQuotedString(rawETag);
}
......@@ -42,17 +55,14 @@ public class GetETag implements Property {
@Override
public GetETag create(XmlPullParser parser) {
GetETag getETag = new GetETag();
// <!ELEMENT getetag (#PCDATA) >
// ETag = "ETag" ":" entity-tag
try {
int eventType = parser.getEventType();
getETag.eTag = parser.nextText();
} catch(XmlPullParserException |IOException e) {
Log.e(Constants.LOG_TAG, "Couldn't parse <getetag>", e);
return new GetETag(parser.nextText());
} catch(XmlPullParserException|IOException e) {
Constants.log.error("Couldn't parse <getetag>", e);
return null;
}
return getETag;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment