1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.syncope.common.rest.api.batch;
20
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.util.ArrayList;
24 import java.util.HashMap;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
32 import javax.ws.rs.HttpMethod;
33 import javax.ws.rs.core.MediaType;
34 import org.apache.commons.lang3.ArrayUtils;
35 import org.apache.commons.lang3.SerializationUtils;
36 import org.apache.commons.lang3.StringUtils;
37 import org.apache.syncope.common.rest.api.RESTHeaders;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 public final class BatchPayloadParser {
42
43 private static final Logger LOG = LoggerFactory.getLogger(BatchPayloadParser.class);
44
45 private static final Pattern PATTERN_LAST_CRLF = Pattern.compile("(.*)\\r\\n\\s*", Pattern.DOTALL);
46
47 private static final Pattern PATTERN_HEADER_LINE = Pattern.compile("((?:\\w|[!#$%\\&'*+\\-.^`|~])+):\\s?(.*)\\s*");
48
49 private static final Pattern PATTERN_BLANK_LINE = Pattern.compile("\\s*\r?\n\\s*");
50
51 private static final String[] HTTP_METHODS = {
52 HttpMethod.DELETE,
53 HttpMethod.PATCH,
54 HttpMethod.POST,
55 HttpMethod.PUT
56 };
57
58 private static BatchPayloadLine removeEndingCRLF(final BatchPayloadLine line) {
59 Matcher matcher = PATTERN_LAST_CRLF.matcher(line.toString());
60 return matcher.matches()
61 ? new BatchPayloadLine(matcher.group(1), line.getLineNumber())
62 : line;
63 }
64
65 private static void removeEndingCRLFFromList(final List<BatchPayloadLine> lines) {
66 if (!lines.isEmpty()) {
67 BatchPayloadLine lastLine = lines.remove(lines.size() - 1);
68 lines.add(removeEndingCRLF(lastLine));
69 }
70 }
71
72 private static List<List<BatchPayloadLine>> split(final List<BatchPayloadLine> lines, final String boundary) {
73 List<List<BatchPayloadLine>> messageParts = new ArrayList<>();
74 List<BatchPayloadLine> currentPart = new ArrayList<>();
75 boolean isEndReached = false;
76
77 String quotedBoundary = Pattern.quote(boundary);
78 Pattern boundaryDelimiterPattern = Pattern.compile("--" + quotedBoundary + "--\\s*");
79 Pattern boundaryPattern = Pattern.compile("--" + quotedBoundary + "\\s*");
80
81 for (BatchPayloadLine line : lines) {
82 if (boundaryDelimiterPattern.matcher(line.toString()).matches()) {
83 removeEndingCRLFFromList(currentPart);
84 messageParts.add(currentPart);
85 isEndReached = true;
86 } else if (boundaryPattern.matcher(line.toString()).matches()) {
87 removeEndingCRLFFromList(currentPart);
88 messageParts.add(currentPart);
89 currentPart = new ArrayList<>();
90 } else {
91 currentPart.add(line);
92 }
93
94 if (isEndReached) {
95 break;
96 }
97 }
98
99
100 if (!messageParts.isEmpty()) {
101 messageParts.remove(0);
102 }
103
104 if (!isEndReached) {
105 int lineNumber = lines.isEmpty() ? 0 : lines.get(0).getLineNumber();
106 throw new IllegalArgumentException("Missing close boundary delimiter around line " + lineNumber);
107 }
108
109 return messageParts;
110 }
111
112 private static void consumeHeaders(final List<BatchPayloadLine> bodyPart, final BatchItem item) {
113 Map<String, List<Object>> headers = new HashMap<>();
114
115 boolean isHeader = true;
116 for (Iterator<BatchPayloadLine> itor = bodyPart.iterator(); itor.hasNext() && isHeader;) {
117 BatchPayloadLine currentLine = itor.next();
118
119 Matcher headerMatcher = PATTERN_HEADER_LINE.matcher(currentLine.toString());
120 if (headerMatcher.matches() && headerMatcher.groupCount() == 2) {
121 itor.remove();
122 } else {
123 isHeader = false;
124 }
125 }
126 consumeBlankLine(bodyPart);
127
128 isHeader = true;
129 for (Iterator<BatchPayloadLine> itor = bodyPart.iterator(); itor.hasNext() && isHeader;) {
130 BatchPayloadLine currentLine = itor.next();
131
132 if (currentLine.toString().contains("HTTP/1.1")) {
133 itor.remove();
134
135 if (ArrayUtils.contains(HTTP_METHODS, StringUtils.substringBefore(currentLine.toString(), " "))
136 && item instanceof BatchRequestItem) {
137
138 BatchRequestItem bri = BatchRequestItem.class.cast(item);
139 String[] parts = currentLine.toString().split(" ");
140 bri.setMethod(parts[0]);
141 String[] target = parts[1].split("\\?");
142 bri.setRequestURI(target[0]);
143 if (target.length > 1) {
144 bri.setQueryString(target[1]);
145 }
146 } else if (item instanceof BatchResponseItem) {
147 BatchResponseItem bri = BatchResponseItem.class.cast(item);
148 try {
149 bri.setStatus(Integer.valueOf(StringUtils.substringBefore(
150 StringUtils.substringAfter(currentLine.toString(), " "), " ").trim()));
151 } catch (NumberFormatException e) {
152 LOG.error("Invalid value found in response for HTTP status", e);
153 }
154 }
155 } else {
156 Matcher headerMatcher = PATTERN_HEADER_LINE.matcher(currentLine.toString());
157 if (headerMatcher.matches() && headerMatcher.groupCount() == 2) {
158 itor.remove();
159
160 String headerName = headerMatcher.group(1).trim();
161 String headerValue = headerMatcher.group(2).trim();
162
163 List<Object> header = headers.get(headerName);
164 if (header == null) {
165 header = new ArrayList<>();
166 headers.put(headerName, header);
167 }
168 header.addAll(Stream.of(headerValue.split(",")).map(String::trim).collect(Collectors.toList()));
169 } else {
170 isHeader = false;
171 }
172 }
173 }
174 consumeBlankLine(bodyPart);
175
176 item.setHeaders(headers);
177 }
178
179 private static void consumeBlankLine(final List<BatchPayloadLine> bodyPart) {
180 if (!bodyPart.isEmpty() && PATTERN_BLANK_LINE.matcher(bodyPart.get(0).toString()).matches()) {
181 bodyPart.remove(0);
182 }
183 }
184
185 public static <T extends BatchItem> List<T> parse(
186 final InputStream in,
187 final MediaType multipartMixed,
188 final T template) throws IOException {
189
190 List<BatchPayloadLine> lines;
191 try (BatchPayloadLineReader lineReader = new BatchPayloadLineReader(in, multipartMixed)) {
192 lines = lineReader.read();
193 }
194
195 return split(lines, multipartMixed.getParameters().get(RESTHeaders.BOUNDARY_PARAMETER)).stream().
196 map(bodyPart -> {
197 LOG.debug("Body part:\n{}", bodyPart);
198
199 T item = SerializationUtils.clone(template);
200
201 consumeHeaders(bodyPart, item);
202 item.setContent(
203 bodyPart.stream().map(BatchPayloadLine::toString).collect(Collectors.joining()));
204
205 return item;
206 }).collect(Collectors.toList());
207 }
208
209 private BatchPayloadParser() {
210
211 }
212 }