/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.jooby.internal;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import org.jooby.MediaType;
import org.jooby.Renderer;
import org.jooby.Sse;
import com.google.common.io.ByteSource;
import javaslang.Function1;
import javaslang.Tuple;
import javaslang.Tuple2;
public class SseRenderer extends AbstractRendererContext {
static final ByteSource ID = bytes("id:");
static final ByteSource EVENT = bytes("event:");
static final ByteSource RETRY = bytes("retry:");
static final ByteSource DATA = bytes("data:");
static final ByteSource COMMENT = bytes(":");
static final byte nl = '\n';
static final ByteSource NL = bytes("\n");
private ByteSource data;
public SseRenderer(final List<Renderer> renderers, final List<MediaType> produces,
final Charset charset, Locale locale, final Map<String, Object> locals) {
super(renderers, produces, charset, locale, locals);
}
public byte[] format(final Sse.Event event) throws Exception {
// comment?
data = event.comment()
.map(comment -> ByteSource.concat(COMMENT, bytes(comment), NL))
.orElse(ByteSource.empty());
// id?
data = event.id()
.map(id -> ByteSource.concat(data, ID, bytes(id.toString()), NL))
.orElse(data);
// event?
data = event.name()
.map(name -> ByteSource.concat(data, EVENT, bytes(name), NL))
.orElse(data);
// retry?
data = event.retry()
.map(retry -> ByteSource.concat(data, RETRY, bytes(Long.toString(retry)), NL))
.orElse(data);
Optional<Object> value = event.data();
if (value.isPresent()) {
render(value.get());
}
data = ByteSource.concat(data, NL);
byte[] bytes = data.read();
data = null;
return bytes;
}
@Override
protected void _send(final byte[] bytes) throws Exception {
List<Tuple2<Integer, Integer>> lines = split(bytes);
if (lines.size() == 1) {
data = ByteSource.concat(data, DATA, ByteSource.wrap(bytes), NL);
} else {
for (Tuple2<Integer, Integer> line : lines) {
data = ByteSource.concat(data, DATA, ByteSource.wrap(bytes)
.slice(line._1, line._2 - line._1), NL);
}
}
}
@Override
protected void _send(final ByteBuffer buffer) throws Exception {
byte[] bytes;
if (buffer.hasArray()) {
_send(buffer.array());
} else {
bytes = new byte[buffer.remaining()];
buffer.get(bytes);
_send(bytes);
}
}
@Override
protected void _send(final FileChannel file) throws Exception {
throw new UnsupportedOperationException();
}
@Override
protected void _send(final InputStream stream) throws Exception {
throw new UnsupportedOperationException();
}
private static ByteSource bytes(final String value) {
return ByteSource.wrap(value.getBytes(StandardCharsets.UTF_8));
}
private static List<Tuple2<Integer, Integer>> split(final byte[] bytes) {
List<Tuple2<Integer, Integer>> range = new ArrayList<>();
Function1<Integer, Integer> nextLine = start -> {
for (int i = start; i < bytes.length; i++) {
if (bytes[i] == nl) {
return i;
}
}
return bytes.length;
};
int from = 0;
int to = nextLine.apply(from);
int len = bytes.length;
range.add(Tuple.of(from, to));
while (to != len) {
from = to + 1;
to = nextLine.apply(from);
if (to > from) {
range.add(Tuple.of(from, to));
}
}
return range;
}
}