/*
* Copyright 2002-2017 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.json;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.PrettyPrinter;
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.EncodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageEncoder;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* Encode from an {@code Object} stream to a byte stream of JSON objects,
* using Jackson 2.9.
*
* @author Sebastien Deleuze
* @author Arjen Poutsma
* @since 5.0
* @see Jackson2JsonDecoder
*/
public class Jackson2JsonEncoder extends Jackson2CodecSupport implements HttpMessageEncoder<Object> {
private final List<MediaType> streamingMediaTypes = new ArrayList<>(1);
private final PrettyPrinter ssePrettyPrinter;
public Jackson2JsonEncoder() {
this(Jackson2ObjectMapperBuilder.json().build());
}
public Jackson2JsonEncoder(ObjectMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
this.streamingMediaTypes.add(MediaType.APPLICATION_STREAM_JSON);
this.ssePrettyPrinter = initSsePrettyPrinter();
}
private static PrettyPrinter initSsePrettyPrinter() {
DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
printer.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
return printer;
}
/**
* Configure "streaming" media types for which flushing should be performed
* automatically vs at the end of the stream.
* <p>By default this is set to {@link MediaType#APPLICATION_STREAM_JSON}.
* @param mediaTypes one or more media types to add to the list
* @see HttpMessageEncoder#getStreamingMediaTypes()
*/
public void setStreamingMediaTypes(List<MediaType> mediaTypes) {
this.streamingMediaTypes.clear();
this.streamingMediaTypes.addAll(mediaTypes);
}
@Override
public List<MimeType> getEncodableMimeTypes() {
return JSON_MIME_TYPES;
}
@Override
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
Class<?> clazz = elementType.resolve(Object.class);
return (Object.class == clazz) ||
!String.class.isAssignableFrom(elementType.resolve(clazz)) &&
this.objectMapper.canSerialize(clazz) && supportsMimeType(mimeType);
}
@Override
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
Assert.notNull(inputStream, "'inputStream' must not be null");
Assert.notNull(bufferFactory, "'bufferFactory' must not be null");
Assert.notNull(elementType, "'elementType' must not be null");
if (inputStream instanceof Mono) {
return Flux.from(inputStream).map(value ->
encodeValue(value, mimeType, bufferFactory, elementType, hints));
}
else if (MediaType.APPLICATION_STREAM_JSON.isCompatibleWith(mimeType)) {
return Flux.from(inputStream).map(value -> {
DataBuffer buffer = encodeValue(value, mimeType, bufferFactory, elementType, hints);
buffer.write(new byte[]{'\n'});
return buffer;
});
}
else {
ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType);
return Flux.from(inputStream).collectList().map(list ->
encodeValue(list, mimeType, bufferFactory, listType, hints)).flux();
}
}
private DataBuffer encodeValue(Object value, MimeType mimeType, DataBufferFactory bufferFactory,
ResolvableType elementType, Map<String, Object> hints) {
TypeFactory typeFactory = this.objectMapper.getTypeFactory();
JavaType javaType = typeFactory.constructType(elementType.getType());
if (elementType.isInstance(value)) {
javaType = getJavaType(elementType.getType(), null);
}
Class<?> jsonView = (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT);
ObjectWriter writer = (jsonView != null ?
this.objectMapper.writerWithView(jsonView) : this.objectMapper.writer());
if (javaType != null && javaType.isContainerType()) {
writer = writer.forType(javaType);
}
if (MediaType.TEXT_EVENT_STREAM.isCompatibleWith(mimeType) &&
writer.getConfig().isEnabled(SerializationFeature.INDENT_OUTPUT)) {
writer = writer.with(this.ssePrettyPrinter);
}
DataBuffer buffer = bufferFactory.allocateBuffer();
OutputStream outputStream = buffer.asOutputStream();
try {
writer.writeValue(outputStream, value);
}
catch (InvalidDefinitionException ex) {
throw new CodecException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex);
}
catch (IOException ex) {
throw new IllegalStateException("Unexpected I/O error while writing to data buffer", ex);
}
return buffer;
}
// HttpMessageEncoder...
@Override
public List<MediaType> getStreamingMediaTypes() {
return Collections.unmodifiableList(this.streamingMediaTypes);
}
@Override
public Map<String, Object> getEncodeHints(ResolvableType actualType, ResolvableType elementType,
MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
return getHints(actualType);
}
@Override
protected <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType) {
return parameter.getMethodAnnotation(annotType);
}
}