/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.AbstractEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.FlushingDataBuffer;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.web.reactive.sse.SseEvent;
/**
* An encoder for {@link SseEvent}s that also supports any other kind of {@link Object}
* (in that case, the object will be the data of the {@link SseEvent}).
* @author Sebastien Deleuze
*/
public class SseEventEncoder extends AbstractEncoder<Object> {
private final List<Encoder<?>> dataEncoders;
public SseEventEncoder(List<Encoder<?>> dataEncoders) {
super(new MimeType("text", "event-stream"));
Assert.notNull(dataEncoders, "'dataEncoders' must not be null");
this.dataEncoders = dataEncoders;
}
@Override
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
ResolvableType type, MimeType sseMimeType, Object... hints) {
return Flux.from(inputStream).flatMap(input -> {
SseEvent event = (SseEvent.class.equals(type.getRawClass()) ?
(SseEvent)input : new SseEvent(input));
StringBuilder sb = new StringBuilder();
if (event.getId() != null) {
sb.append("id:");
sb.append(event.getId());
sb.append("\n");
}
if (event.getName() != null) {
sb.append("event:");
sb.append(event.getName());
sb.append("\n");
}
if (event.getReconnectTime() != null) {
sb.append("retry:");
sb.append(event.getReconnectTime().toString());
sb.append("\n");
}
if (event.getComment() != null) {
sb.append(":");
sb.append(event.getComment().replaceAll("\\n", "\n:"));
sb.append("\n");
}
Object data = event.getData();
Flux<DataBuffer> dataBuffer = Flux.empty();
MediaType mediaType = (event.getMediaType() == null ?
MediaType.ALL : event.getMediaType());
if (data != null) {
sb.append("data:");
if (data instanceof String) {
sb.append(((String)data).replaceAll("\\n", "\ndata:")).append("\n");
}
else {
Optional<Encoder<?>> encoder = dataEncoders
.stream()
.filter(e -> e.canEncode(ResolvableType.forClass(data.getClass()), mediaType))
.findFirst();
if (encoder.isPresent()) {
dataBuffer = ((Encoder<Object>)encoder.get())
.encode(Mono.just(data), bufferFactory,
ResolvableType.forClass(data.getClass()), mediaType)
.concatWith(encodeString("\n", bufferFactory));
}
else {
throw new CodecException("No suitable encoder found!");
}
}
}
// Keep the SSE connection open even for cold stream in order to avoid
// unexpected browser reconnection
return Flux.concat(
encodeString(sb.toString(), bufferFactory),
dataBuffer,
encodeString("\n", bufferFactory),
Mono.just(FlushingDataBuffer.INSTANCE),
Flux.never()
);
});
}
private Mono<DataBuffer> encodeString(String str, DataBufferFactory bufferFactory) {
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length).write(bytes);
return Mono.just(buffer);
}
}