|
2 | 2 |
|
3 | 3 | import org.slf4j.Logger; |
4 | 4 | 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; |
8 | 8 |
|
| 9 | +import javax.xml.parsers.ParserConfigurationException; |
| 10 | +import javax.xml.parsers.SAXParser; |
| 11 | +import javax.xml.parsers.SAXParserFactory; |
9 | 12 | import java.io.IOException; |
10 | 13 | import java.io.InputStream; |
11 | | -import java.text.ParseException; |
12 | | -import java.text.SimpleDateFormat; |
| 14 | +import java.time.DateTimeException; |
13 | 15 | import java.time.Instant; |
| 16 | +import java.time.format.DateTimeFormatter; |
14 | 17 | import java.util.ArrayList; |
15 | 18 | import java.util.List; |
16 | | -import java.util.Locale; |
17 | 19 | import java.util.Optional; |
18 | 20 |
|
19 | | -import static java.lang.String.format; |
20 | | - |
21 | 21 | class PropfindResponseParser { |
22 | 22 |
|
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 | + |
190 | 160 |
|
191 | 161 | } |
0 commit comments