| 
16 | 16 | 
 
  | 
17 | 17 | package org.springframework.http.codec;  | 
18 | 18 | 
 
  | 
19 |  | -import java.nio.CharBuffer;  | 
20 | 19 | import java.nio.charset.StandardCharsets;  | 
21 | 20 | import java.time.Duration;  | 
22 |  | -import java.util.ArrayList;  | 
23 | 21 | import java.util.Collections;  | 
24 | 22 | import java.util.List;  | 
25 | 23 | import java.util.Map;  | 
26 |  | -import java.util.function.IntPredicate;  | 
27 |  | -import java.util.stream.Collectors;  | 
28 | 24 | 
 
  | 
29 | 25 | import reactor.core.publisher.Flux;  | 
30 | 26 | import reactor.core.publisher.Mono;  | 
 | 
35 | 31 | import org.springframework.core.codec.StringDecoder;  | 
36 | 32 | import org.springframework.core.io.buffer.DataBuffer;  | 
37 | 33 | import org.springframework.core.io.buffer.DataBufferFactory;  | 
38 |  | -import org.springframework.core.io.buffer.DataBufferUtils;  | 
39 | 34 | import org.springframework.core.io.buffer.DefaultDataBufferFactory;  | 
40 | 35 | import org.springframework.http.MediaType;  | 
41 | 36 | import org.springframework.http.ReactiveHttpInputMessage;  | 
 | 
51 | 46 |  */  | 
52 | 47 | public class ServerSentEventHttpMessageReader implements HttpMessageReader<Object> {  | 
53 | 48 | 
 
  | 
54 |  | -	private static final IntPredicate NEWLINE_DELIMITER = b -> b == '\n' || b == '\r';  | 
55 |  | - | 
56 | 49 | 	private static final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();  | 
57 | 50 | 
 
  | 
58 | 51 | 	private static final StringDecoder stringDecoder = StringDecoder.textPlainOnly();  | 
59 | 52 | 
 
  | 
 | 53 | +	private static final ResolvableType STRING_TYPE = ResolvableType.forClass(String.class);  | 
 | 54 | + | 
60 | 55 | 
 
  | 
61 | 56 | 	@Nullable  | 
62 | 57 | 	private final Decoder<?> decoder;  | 
@@ -110,77 +105,53 @@ public Flux<Object> read(ResolvableType elementType, ReactiveHttpInputMessage me  | 
110 | 105 | 		boolean shouldWrap = isServerSentEvent(elementType);  | 
111 | 106 | 		ResolvableType valueType = (shouldWrap ? elementType.getGeneric(0) : elementType);  | 
112 | 107 | 
 
  | 
113 |  | -		return Flux.from(message.getBody())  | 
114 |  | -				.concatMap(ServerSentEventHttpMessageReader::splitOnNewline)  | 
115 |  | -				.map(buffer -> {  | 
116 |  | -					CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());  | 
117 |  | -					DataBufferUtils.release(buffer);  | 
118 |  | -					return charBuffer.toString();  | 
119 |  | -				})  | 
120 |  | -				.bufferUntil(line -> line.equals("\n"))  | 
121 |  | -				.concatMap(rawLines -> {  | 
122 |  | -					String[] lines = rawLines.stream().collect(Collectors.joining()).split("\\r?\\n");  | 
123 |  | -					return buildEvent(lines, valueType, hints)  | 
124 |  | -							.filter(event -> shouldWrap || event.data() != null)  | 
125 |  | -							.map(event -> shouldWrap ? event : event.data());  | 
126 |  | -				})  | 
127 |  | -				.cast(Object.class);  | 
128 |  | -	}  | 
129 |  | - | 
130 |  | -	private static Flux<DataBuffer> splitOnNewline(DataBuffer dataBuffer) {  | 
131 |  | -		List<DataBuffer> results = new ArrayList<>();  | 
132 |  | -		int startIdx = 0;  | 
133 |  | -		int endIdx;  | 
134 |  | -		final int limit = dataBuffer.readableByteCount();  | 
135 |  | -		do {  | 
136 |  | -			endIdx = dataBuffer.indexOf(NEWLINE_DELIMITER, startIdx);  | 
137 |  | -			int length = endIdx != -1 ? endIdx - startIdx + 1 : limit - startIdx;  | 
138 |  | -			DataBuffer token = dataBuffer.slice(startIdx, length);  | 
139 |  | -			results.add(DataBufferUtils.retain(token));  | 
140 |  | -			startIdx = endIdx + 1;  | 
141 |  | -		}  | 
142 |  | -		while (startIdx < limit && endIdx != -1);  | 
143 |  | -		DataBufferUtils.release(dataBuffer);  | 
144 |  | -		return Flux.fromIterable(results);  | 
 | 108 | +		return stringDecoder.decode(message.getBody(), STRING_TYPE, null, Collections.emptyMap())  | 
 | 109 | +				.bufferUntil(line -> line.equals(""))  | 
 | 110 | +				.concatMap(lines -> buildEvent(lines, valueType, shouldWrap, hints));  | 
145 | 111 | 	}  | 
146 | 112 | 
 
  | 
147 |  | -	private Mono<ServerSentEvent<Object>> buildEvent(String[] lines, ResolvableType valueType,  | 
 | 113 | +	private Mono<?> buildEvent(List<String> lines, ResolvableType valueType, boolean shouldWrap,  | 
148 | 114 | 			Map<String, Object> hints) {  | 
149 | 115 | 
 
  | 
150 |  | -		ServerSentEvent.Builder<Object> sseBuilder = ServerSentEvent.builder();  | 
 | 116 | +		ServerSentEvent.Builder<Object> sseBuilder = shouldWrap ? ServerSentEvent.builder() : null;  | 
151 | 117 | 		StringBuilder data = null;  | 
152 | 118 | 		StringBuilder comment = null;  | 
153 | 119 | 
 
  | 
154 | 120 | 		for (String line : lines) {  | 
155 |  | -			if (line.startsWith("id:")) {  | 
156 |  | -				sseBuilder.id(line.substring(3));  | 
157 |  | -			}  | 
158 |  | -			else if (line.startsWith("event:")) {  | 
159 |  | -				sseBuilder.event(line.substring(6));  | 
160 |  | -			}  | 
161 |  | -			else if (line.startsWith("data:")) {  | 
 | 121 | +			if (line.startsWith("data:")) {  | 
162 | 122 | 				data = (data != null ? data : new StringBuilder());  | 
163 | 123 | 				data.append(line.substring(5)).append("\n");  | 
164 | 124 | 			}  | 
165 |  | -			else if (line.startsWith("retry:")) {  | 
166 |  | -				sseBuilder.retry(Duration.ofMillis(Long.valueOf(line.substring(6))));  | 
167 |  | -			}  | 
168 |  | -			else if (line.startsWith(":")) {  | 
169 |  | -				comment = (comment != null ? comment : new StringBuilder());  | 
170 |  | -				comment.append(line.substring(1)).append("\n");  | 
 | 125 | +			if (shouldWrap) {  | 
 | 126 | +				if (line.startsWith("id:")) {  | 
 | 127 | +					sseBuilder.id(line.substring(3));  | 
 | 128 | +				}  | 
 | 129 | +				else if (line.startsWith("event:")) {  | 
 | 130 | +					sseBuilder.event(line.substring(6));  | 
 | 131 | +				}  | 
 | 132 | +				else if (line.startsWith("retry:")) {  | 
 | 133 | +					sseBuilder.retry(Duration.ofMillis(Long.valueOf(line.substring(6))));  | 
 | 134 | +				}  | 
 | 135 | +				else if (line.startsWith(":")) {  | 
 | 136 | +					comment = (comment != null ? comment : new StringBuilder());  | 
 | 137 | +					comment.append(line.substring(1)).append("\n");  | 
 | 138 | +				}  | 
171 | 139 | 			}  | 
172 | 140 | 		}  | 
173 |  | -		if (comment != null) {  | 
174 |  | -			sseBuilder.comment(comment.toString().substring(0, comment.length() - 1));  | 
175 |  | -		}  | 
176 |  | -		if (data != null) {  | 
177 |  | -			return decodeData(data.toString(), valueType, hints).map(decodedData -> {  | 
178 |  | -				sseBuilder.data(decodedData);  | 
 | 141 | + | 
 | 142 | +		Mono<?> decodedData = (data != null ? decodeData(data.toString(), valueType, hints) : Mono.empty());  | 
 | 143 | + | 
 | 144 | +		if (shouldWrap) {  | 
 | 145 | +			if (comment != null) {  | 
 | 146 | +				sseBuilder.comment(comment.toString().substring(0, comment.length() - 1));  | 
 | 147 | +			}  | 
 | 148 | +			return decodedData.map(o -> {  | 
 | 149 | +				sseBuilder.data(o);  | 
179 | 150 | 				return sseBuilder.build();  | 
180 | 151 | 			});  | 
181 | 152 | 		}  | 
182 | 153 | 		else {  | 
183 |  | -			return Mono.just(sseBuilder.build());  | 
 | 154 | +			return decodedData;  | 
184 | 155 | 		}  | 
185 | 156 | 	}  | 
186 | 157 | 
 
  | 
 | 
0 commit comments