Skip to content
This repository was archived by the owner on Mar 14, 2025. It is now read-only.

Commit 23d4f43

Browse files
use sax instead of kxml2, reintroduce module-info
1 parent f1c0511 commit 23d4f43

File tree

6 files changed

+168
-195
lines changed

6 files changed

+168
-195
lines changed

pom.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,6 @@
8181
<artifactId>okhttp-digest</artifactId>
8282
<version>${okhttp-digest.version}</version>
8383
</dependency>
84-
<dependency>
85-
<groupId>net.sf.kxml</groupId>
86-
<artifactId>kxml2</artifactId>
87-
<version>${kxml2.version}</version>
88-
</dependency>
8984

9085
<!-- Test -->
9186
<dependency>

src/main/java/module-info.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
open module org.cryptomator.cloudaccess {
2+
exports org.cryptomator.cloudaccess;
3+
exports org.cryptomator.cloudaccess.api;
4+
exports org.cryptomator.cloudaccess.api.exceptions;
5+
6+
requires java.xml;
7+
requires com.google.common;
8+
requires org.slf4j;
9+
requires okhttp3;
10+
requires okhttp.digest;
11+
requires okio;
12+
}

src/main/java/org/cryptomator/cloudaccess/webdav/PropfindResponseParser.java

Lines changed: 141 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -2,190 +2,156 @@
22

33
import org.slf4j.Logger;
44
import org.slf4j.LoggerFactory;
5-
import org.xmlpull.v1.XmlPullParser;
6-
import org.xmlpull.v1.XmlPullParserException;
7-
import org.xmlpull.v1.XmlPullParserFactory;
5+
import org.xml.sax.Attributes;
6+
import org.xml.sax.SAXException;
7+
import org.xml.sax.helpers.DefaultHandler;
88

9+
import javax.xml.parsers.ParserConfigurationException;
10+
import javax.xml.parsers.SAXParser;
11+
import javax.xml.parsers.SAXParserFactory;
912
import java.io.IOException;
1013
import java.io.InputStream;
11-
import java.text.ParseException;
12-
import java.text.SimpleDateFormat;
14+
import java.time.DateTimeException;
1315
import java.time.Instant;
16+
import java.time.format.DateTimeFormatter;
1417
import java.util.ArrayList;
1518
import java.util.List;
16-
import java.util.Locale;
1719
import java.util.Optional;
1820

19-
import static java.lang.String.format;
20-
2121
class PropfindResponseParser {
2222

23-
private static final Logger LOG = LoggerFactory.getLogger(PropfindResponseParser.class);
24-
25-
private static final String TAG_RESPONSE = "response";
26-
private static final String TAG_HREF = "href";
27-
private static final String TAG_COLLECTION = "collection";
28-
private static final String TAG_LAST_MODIFIED = "getlastmodified";
29-
private static final String TAG_CONTENT_LENGTH = "getcontentlength";
30-
private static final String TAG_PROPSTAT = "propstat";
31-
private static final String TAG_STATUS = "status";
32-
private static final String STATUS_OK = "200";
33-
34-
private final XmlPullParser xmlPullParser;
35-
36-
PropfindResponseParser() {
37-
try {
38-
this.xmlPullParser = XmlPullParserFactory.newInstance().newPullParser();
39-
} catch (XmlPullParserException e) {
40-
throw new IllegalStateException(e);
41-
}
42-
}
43-
44-
List<PropfindEntryData> parse(final InputStream responseBody) throws XmlPullParserException, IOException {
45-
final var entryData = new ArrayList<PropfindEntryData>();
46-
xmlPullParser.setInput(responseBody, "UTF-8");
47-
48-
while (skipToStartOf(TAG_RESPONSE)) {
49-
final var entry = parseResponse();
50-
if (entry != null) {
51-
entryData.add(entry);
52-
}
53-
}
54-
55-
return entryData;
56-
}
57-
58-
private boolean skipToStartOf(final String tag) throws XmlPullParserException, IOException {
59-
do {
60-
xmlPullParser.next();
61-
} while (!endOfDocument() && !startOf(tag));
62-
return startOf(tag);
63-
}
64-
65-
private PropfindEntryData parseResponse() throws XmlPullParserException, IOException {
66-
PropfindEntryData entry = null;
67-
String path = null;
68-
69-
while (nextTagUntilEndOf(TAG_RESPONSE)) {
70-
if (tagIs(TAG_PROPSTAT)) {
71-
entry = defaultIfNull(parsePropstatWith200Status(), entry);
72-
} else if (tagIs(TAG_HREF)) {
73-
path = textInCurrentTag().trim();
74-
}
75-
}
76-
77-
if (entry == null) {
78-
LOG.warn("No propstat element with 200 status in response element. Entry ignored.");
79-
LOG.debug(format("No propstat element with 200 status in response element. Entry ignored. Path: %s", path));
80-
return null;
81-
}
82-
if (path == null) {
83-
LOG.warn("Missing href in response element. Entry ignored.");
84-
return null;
85-
}
86-
87-
entry.setPath(path);
88-
return entry;
89-
}
90-
91-
private PropfindEntryData parsePropstatWith200Status() throws IOException, XmlPullParserException {
92-
final var result = new PropfindEntryData();
93-
var statusOk = false;
94-
while (nextTagUntilEndOf(TAG_PROPSTAT)) {
95-
if (tagIs(TAG_STATUS)) {
96-
String text = textInCurrentTag().trim();
97-
String[] statusSegments = text.split(" ");
98-
String code = statusSegments.length > 0 ? statusSegments[1] : "";
99-
statusOk = STATUS_OK.equals(code);
100-
} else if (tagIs(TAG_COLLECTION)) {
101-
result.setFile(false);
102-
} else if (tagIs(TAG_LAST_MODIFIED)) {
103-
result.setLastModified(parseDate(textInCurrentTag()));
104-
} else if (tagIs(TAG_CONTENT_LENGTH)) {
105-
result.setSize(parseLong(textInCurrentTag()));
106-
}
107-
}
108-
if (statusOk) {
109-
return result;
110-
} else {
111-
return null;
112-
}
113-
}
114-
115-
private boolean nextTagUntilEndOf(final String tag) throws XmlPullParserException, IOException {
116-
do {
117-
xmlPullParser.next();
118-
} while (!endOfDocument() && !startOfATag() && !endOf(tag));
119-
return startOfATag();
120-
}
121-
122-
private boolean startOf(String tag) throws XmlPullParserException {
123-
return startOfATag() && tagIs(tag);
124-
}
125-
126-
private boolean tagIs(String tag) {
127-
return tag.equalsIgnoreCase(localName());
128-
}
129-
130-
private boolean startOfATag() throws XmlPullParserException {
131-
return xmlPullParser.getEventType() == XmlPullParser.START_TAG;
132-
}
133-
134-
private boolean endOf(String tag) throws XmlPullParserException {
135-
return xmlPullParser.getEventType() == XmlPullParser.END_TAG && tag.equalsIgnoreCase(localName());
136-
}
137-
138-
private String localName() {
139-
final var rawName = xmlPullParser.getName();
140-
final var namespaceAndLocalName = rawName.split(":", 2);
141-
return namespaceAndLocalName[namespaceAndLocalName.length - 1];
142-
}
143-
144-
private boolean endOfDocument() throws XmlPullParserException {
145-
return xmlPullParser.getEventType() == XmlPullParser.END_DOCUMENT;
146-
}
147-
148-
private String textInCurrentTag() throws IOException, XmlPullParserException {
149-
if (!startOfATag()) {
150-
throw new IllegalStateException("textInCurrentTag may only be called at start of a tag");
151-
}
152-
final var result = new StringBuilder();
153-
var ident = 0;
154-
do {
155-
switch (xmlPullParser.next()) {
156-
case XmlPullParser.TEXT:
157-
result.append(xmlPullParser.getText());
158-
break;
159-
case XmlPullParser.START_TAG:
160-
ident++;
161-
break;
162-
case XmlPullParser.END_TAG:
163-
ident--;
164-
break;
165-
}
166-
} while (!endOfDocument() && ident >= 0);
167-
return result.toString();
168-
}
169-
170-
private PropfindEntryData defaultIfNull(final PropfindEntryData value, final PropfindEntryData defaultValue) {
171-
return value == null ? defaultValue : value;
172-
}
173-
174-
private Optional<Instant> parseDate(final String text) {
175-
try {
176-
final var RFC_1123_DATE_TIME = "EEE, dd MMM yyyy HH:mm:ss z";
177-
return Optional.of(new SimpleDateFormat(RFC_1123_DATE_TIME, Locale.US).parse(text).toInstant());
178-
} catch (IllegalArgumentException | ParseException e) {
179-
return Optional.empty();
180-
}
181-
}
182-
183-
private Optional<Long> parseLong(final String text) {
184-
try {
185-
return Optional.of(Long.parseLong(text));
186-
} catch (NumberFormatException e) {
187-
return Optional.empty();
188-
}
189-
}
23+
private static final Logger LOG = LoggerFactory.getLogger(PropfindResponseParser.class);
24+
25+
private static final SAXParserFactory PARSER_FACTORY = SAXParserFactory.newInstance();
26+
private static final String TAG_RESPONSE = "response";
27+
private static final String TAG_HREF = "href";
28+
private static final String TAG_COLLECTION = "collection";
29+
private static final String TAG_LAST_MODIFIED = "getlastmodified";
30+
private static final String TAG_CONTENT_LENGTH = "getcontentlength";
31+
private static final String TAG_PROPSTAT = "propstat";
32+
private static final String TAG_STATUS = "status";
33+
private static final String STATUS_OK = "200";
34+
35+
static {
36+
PARSER_FACTORY.setNamespaceAware(true);
37+
}
38+
39+
private final SAXParser parser;
40+
private final List<PropfindEntryData> entries = new ArrayList<>();
41+
42+
PropfindResponseParser() {
43+
try {
44+
this.parser = PARSER_FACTORY.newSAXParser();
45+
} catch (ParserConfigurationException | SAXException e) {
46+
throw new IllegalStateException(e);
47+
}
48+
}
49+
50+
public List<PropfindEntryData> parse(final InputStream responseBody) throws SAXException, IOException {
51+
parser.parse(responseBody, new ParseHandler());
52+
return entries;
53+
}
54+
55+
private class ParseHandler extends DefaultHandler {
56+
57+
private StringBuilder textBuffer;
58+
private String href;
59+
private String lastModified;
60+
private String contentLength;
61+
private String status;
62+
private boolean isCollection;
63+
64+
@Override
65+
public void startElement(String uri, String localName, String qName, Attributes attributes) {
66+
switch (localName.toLowerCase()) {
67+
case TAG_RESPONSE:
68+
href = null;
69+
lastModified = null;
70+
contentLength = null;
71+
status = null;
72+
isCollection = false;
73+
break;
74+
case TAG_HREF:
75+
case TAG_LAST_MODIFIED:
76+
case TAG_CONTENT_LENGTH:
77+
case TAG_STATUS:
78+
textBuffer = new StringBuilder();
79+
break;
80+
case TAG_COLLECTION:
81+
isCollection = true;
82+
break;
83+
default:
84+
// no-op
85+
}
86+
}
87+
88+
@Override
89+
public void characters(char[] ch, int start, int length) {
90+
if (textBuffer != null) {
91+
textBuffer.append(ch, start, length);
92+
}
93+
}
94+
95+
@Override
96+
public void endElement(String uri, String localName, String qName) {
97+
switch (localName.toLowerCase()) {
98+
case TAG_PROPSTAT:
99+
assembleEntry();
100+
break;
101+
case TAG_HREF:
102+
href = textBuffer.toString();
103+
break;
104+
case TAG_LAST_MODIFIED:
105+
lastModified = textBuffer.toString();
106+
break;
107+
case TAG_CONTENT_LENGTH:
108+
contentLength = textBuffer.toString();
109+
break;
110+
case TAG_STATUS:
111+
status = textBuffer.toString();
112+
break;
113+
default:
114+
// no-op
115+
}
116+
}
117+
118+
private void assembleEntry() {
119+
if (!status.contains(STATUS_OK)) {
120+
LOG.warn("No propstat element with 200 status in response element. Entry ignored.");
121+
return; // no-op
122+
}
123+
124+
if (href == null) {
125+
LOG.warn("Missing href in response element. Entry ignored.");
126+
return; // no-op
127+
}
128+
129+
var entry = new PropfindEntryData();
130+
entry.setLastModified(parseDate(lastModified));
131+
entry.setSize(parseLong(contentLength));
132+
entry.setPath(href);
133+
entry.setFile(!isCollection);
134+
135+
entries.add(entry);
136+
}
137+
138+
private Optional<Instant> parseDate(final String text) {
139+
try {
140+
return Optional.of(Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text)));
141+
} catch (DateTimeException e) {
142+
return Optional.empty();
143+
}
144+
}
145+
146+
private Optional<Long> parseLong(final String text) {
147+
try {
148+
return Optional.of(Long.parseLong(text));
149+
} catch (NumberFormatException e) {
150+
return Optional.empty();
151+
}
152+
}
153+
154+
}
155+
190156

191157
}

src/main/java/org/cryptomator/cloudaccess/webdav/WebDavClient.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
import org.cryptomator.cloudaccess.api.CloudItemList;
88
import org.cryptomator.cloudaccess.api.CloudItemMetadata;
99
import org.cryptomator.cloudaccess.api.ProgressListener;
10-
import org.cryptomator.cloudaccess.api.exceptions.CloudProviderException;
1110
import org.cryptomator.cloudaccess.api.exceptions.AlreadyExistsException;
11+
import org.cryptomator.cloudaccess.api.exceptions.CloudProviderException;
1212
import org.cryptomator.cloudaccess.api.exceptions.InsufficientStorageException;
1313
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
14-
import org.xmlpull.v1.XmlPullParserException;
14+
import org.xml.sax.SAXException;
1515

1616
import java.io.IOException;
1717
import java.io.InputStream;
@@ -70,7 +70,7 @@ private CloudItemList list(final Path folder, final PROPFIND_DEPTH propfind_dept
7070
final var nodes = getEntriesFromResponse(response);
7171

7272
return processDirList(nodes);
73-
} catch (IOException | XmlPullParserException e) {
73+
} catch (IOException | SAXException e) {
7474
throw new CloudProviderException(e);
7575
}
7676
}
@@ -82,7 +82,7 @@ CloudItemMetadata itemMetadata(final Path path) throws CloudProviderException {
8282
final var nodes = getEntriesFromResponse(response);
8383

8484
return processGet(nodes);
85-
} catch (IOException | XmlPullParserException e) {
85+
} catch (IOException | SAXException e) {
8686
throw new CloudProviderException(e);
8787
}
8888
}
@@ -105,7 +105,7 @@ private Response executePropfindRequest(final Path path, final PROPFIND_DEPTH pr
105105
return httpClient.execute(builder);
106106
}
107107

108-
private List<PropfindEntryData> getEntriesFromResponse(final Response response) throws IOException, XmlPullParserException {
108+
private List<PropfindEntryData> getEntriesFromResponse(final Response response) throws IOException, SAXException {
109109
try(final var responseBody = response.body()) {
110110
return new PropfindResponseParser().parse(responseBody.byteStream());
111111
}

0 commit comments

Comments
 (0)