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.nio.ByteBuffer;
24  import java.nio.charset.Charset;
25  import java.nio.charset.StandardCharsets;
26  import java.util.ArrayList;
27  import java.util.List;
28  import java.util.Optional;
29  import javax.ws.rs.core.HttpHeaders;
30  import javax.ws.rs.core.MediaType;
31  import org.apache.syncope.common.rest.api.RESTHeaders;
32  import org.apache.syncope.common.rest.api.service.JAXRSService;
33  
34  public class BatchPayloadLineReader implements AutoCloseable {
35  
36      private static final byte CR = '\r';
37  
38      private static final byte LF = '\n';
39  
40      private static final int EOF = -1;
41  
42      private static final int BUFFER_SIZE = 8192;
43  
44      private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
45  
46      private final ReadState readState = new ReadState();
47  
48      private final InputStream in;
49  
50      private final MediaType multipartMixed;
51  
52      private final byte[] buffer = new byte[BUFFER_SIZE];
53  
54      private Charset currentCharset = DEFAULT_CHARSET;
55  
56      private String currentBoundary = null;
57  
58      private int offset = 0;
59  
60      private int limit = 0;
61  
62      public BatchPayloadLineReader(final InputStream in, final MediaType multipartMixed) {
63          this.in = in;
64          this.multipartMixed = multipartMixed;
65      }
66  
67      @Override
68      public void close() throws IOException {
69          in.close();
70      }
71  
72      private boolean isBoundary(final String currentLine) {
73          return (currentBoundary + JAXRSService.CRLF).equals(currentLine)
74                  || (currentBoundary + JAXRSService.DOUBLE_DASH + JAXRSService.CRLF).equals(currentLine);
75      }
76  
77      private int fillBuffer() throws IOException {
78          limit = in.read(buffer, 0, buffer.length);
79          offset = 0;
80  
81          return limit;
82      }
83  
84      private String readLine() throws IOException {
85          if (limit == EOF) {
86              return null;
87          }
88  
89          ByteBuffer innerBuffer = ByteBuffer.allocate(BUFFER_SIZE);
90          // EOF will be considered as line ending
91          boolean foundLineEnd = false;
92  
93          while (!foundLineEnd) {
94              // Is buffer refill required?
95              if (limit == offset && fillBuffer() == EOF) {
96                  foundLineEnd = true;
97              }
98  
99              if (!foundLineEnd) {
100                 byte currentChar = buffer[offset++];
101                 if (!innerBuffer.hasRemaining()) {
102                     innerBuffer.flip();
103                     ByteBuffer tmp = ByteBuffer.allocate(innerBuffer.limit() * 2);
104                     tmp.put(innerBuffer);
105                     innerBuffer = tmp;
106                 }
107                 innerBuffer.put(currentChar);
108 
109                 if (currentChar == LF) {
110                     foundLineEnd = true;
111                 } else if (currentChar == CR) {
112                     foundLineEnd = true;
113 
114                     // Check next byte. Consume \n if available
115                     // Is buffer refill required?
116                     if (limit == offset) {
117                         fillBuffer();
118                     }
119 
120                     // Check if there is at least one character
121                     if (limit != EOF && buffer[offset] == LF) {
122                         innerBuffer.put(LF);
123                         offset++;
124                     }
125                 }
126             }
127         }
128 
129         if (innerBuffer.position() == 0) {
130             return null;
131         } else {
132             String currentLine = new String(innerBuffer.array(), 0, innerBuffer.position(),
133                     readState.isReadBody() ? currentCharset : DEFAULT_CHARSET);
134 
135             if (currentLine.startsWith(HttpHeaders.CONTENT_TYPE)) {
136                 String charsetString = multipartMixed.getParameters().get(MediaType.CHARSET_PARAMETER);
137                 currentCharset = Optional.ofNullable(charsetString).map(Charset::forName).orElse(DEFAULT_CHARSET);
138 
139                 currentBoundary = JAXRSService.DOUBLE_DASH + multipartMixed.getParameters().
140                         get(RESTHeaders.BOUNDARY_PARAMETER);
141             } else if (JAXRSService.CRLF.equals(currentLine)) {
142                 readState.foundLinebreak();
143             } else if (isBoundary(currentLine)) {
144                 readState.foundBoundary();
145             }
146 
147             return currentLine;
148         }
149     }
150 
151     public List<BatchPayloadLine> read() throws IOException {
152         List<BatchPayloadLine> result = new ArrayList<>();
153 
154         String currentLine = readLine();
155         if (currentLine != null) {
156             currentBoundary = currentLine.trim();
157             int counter = 1;
158             result.add(new BatchPayloadLine(currentLine, counter++));
159 
160             while ((currentLine = readLine()) != null) {
161                 result.add(new BatchPayloadLine(currentLine, counter++));
162             }
163         }
164 
165         return result;
166     }
167 
168     /**
169      * Read state indicator (whether currently the {@code body} or {@code header} part is read).
170      */
171     private static class ReadState {
172 
173         private int state = 0;
174 
175         public void foundLinebreak() {
176             state++;
177         }
178 
179         public void foundBoundary() {
180             state = 0;
181         }
182 
183         public boolean isReadBody() {
184             return state >= 2;
185         }
186 
187         @Override
188         public String toString() {
189             return String.valueOf(state);
190         }
191     }
192 }