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

Commit 0b44c56

Browse files
Merge branch 'develop' into release/0.1.0
2 parents fc17d6d + 04369cb commit 0b44c56

16 files changed

+383
-386
lines changed

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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+
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: 145 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -2,190 +2,160 @@
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+
41+
PropfindResponseParser() {
42+
try {
43+
this.parser = PARSER_FACTORY.newSAXParser();
44+
} catch (ParserConfigurationException | SAXException e) {
45+
throw new IllegalStateException(e);
46+
}
47+
}
48+
49+
public List<PropfindEntryData> parse(final InputStream responseBody) throws SAXException, IOException {
50+
if (responseBody == null) {
51+
return List.of();
52+
}
53+
var parseHandler = new ParseHandler();
54+
parser.parse(responseBody, parseHandler);
55+
return parseHandler.entries;
56+
}
57+
58+
private class ParseHandler extends DefaultHandler {
59+
60+
public final List<PropfindEntryData> entries = new ArrayList<>();
61+
private StringBuilder textBuffer;
62+
private String href;
63+
private String lastModified;
64+
private String contentLength;
65+
private String status;
66+
private boolean isCollection;
67+
68+
@Override
69+
public void startElement(String uri, String localName, String qName, Attributes attributes) {
70+
switch (localName.toLowerCase()) {
71+
case TAG_RESPONSE:
72+
href = null;
73+
lastModified = null;
74+
contentLength = null;
75+
status = null;
76+
isCollection = false;
77+
break;
78+
case TAG_HREF:
79+
case TAG_LAST_MODIFIED:
80+
case TAG_CONTENT_LENGTH:
81+
case TAG_STATUS:
82+
textBuffer = new StringBuilder();
83+
break;
84+
case TAG_COLLECTION:
85+
isCollection = true;
86+
break;
87+
default:
88+
// no-op
89+
}
90+
}
91+
92+
@Override
93+
public void characters(char[] ch, int start, int length) {
94+
if (textBuffer != null) {
95+
textBuffer.append(ch, start, length);
96+
}
97+
}
98+
99+
@Override
100+
public void endElement(String uri, String localName, String qName) {
101+
switch (localName.toLowerCase()) {
102+
case TAG_PROPSTAT:
103+
assembleEntry();
104+
break;
105+
case TAG_HREF:
106+
href = textBuffer.toString();
107+
break;
108+
case TAG_LAST_MODIFIED:
109+
lastModified = textBuffer.toString();
110+
break;
111+
case TAG_CONTENT_LENGTH:
112+
contentLength = textBuffer.toString();
113+
break;
114+
case TAG_STATUS:
115+
status = textBuffer.toString();
116+
break;
117+
default:
118+
// no-op
119+
}
120+
}
121+
122+
private void assembleEntry() {
123+
if (!status.contains(STATUS_OK)) {
124+
LOG.warn("No propstat element with 200 status in response element. Entry ignored.");
125+
return; // no-op
126+
}
127+
128+
if (href == null) {
129+
LOG.warn("Missing href in response element. Entry ignored.");
130+
return; // no-op
131+
}
132+
133+
var entry = new PropfindEntryData();
134+
entry.setLastModified(parseDate(lastModified));
135+
entry.setSize(parseLong(contentLength));
136+
entry.setPath(href);
137+
entry.setFile(!isCollection);
138+
139+
entries.add(entry);
140+
}
141+
142+
private Optional<Instant> parseDate(final String text) {
143+
try {
144+
return Optional.of(Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text)));
145+
} catch (DateTimeException e) {
146+
return Optional.empty();
147+
}
148+
}
149+
150+
private Optional<Long> parseLong(final String text) {
151+
try {
152+
return Optional.of(Long.parseLong(text));
153+
} catch (NumberFormatException e) {
154+
return Optional.empty();
155+
}
156+
}
157+
158+
}
159+
190160

191161
}

0 commit comments

Comments
 (0)