View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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          // Remove preamble
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         // private constructor for static utility class
211     }
212 }