package org.zalando.riptide.stream; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Stream; import java.util.stream.StreamSupport; import static org.zalando.riptide.stream.Streams.APPLICATION_JSON_SEQ; import static org.zalando.riptide.stream.Streams.APPLICATION_X_JSON_STREAM; // TODO change to GenericHttpMessageConverter<Stream<T>> final class StreamConverter<T> implements GenericHttpMessageConverter<T> { private static final List<MediaType> DEFAULT_MEDIA_TYPES = Collections.unmodifiableList(Arrays.asList(APPLICATION_JSON_SEQ, APPLICATION_X_JSON_STREAM)); private final ObjectMapper mapper; private final List<MediaType> supportedMediaTypes; /** * @deprecated Use {@link Streams#streamConverter(ObjectMapper)} */ @Deprecated public StreamConverter() { this(null, null); } /** * @deprecated Use {@link Streams#streamConverter(ObjectMapper, List)} * @param mapper * @param supportedMediaTypes */ @Deprecated public StreamConverter(@Nullable final ObjectMapper mapper, @Nullable final List<MediaType> supportedMediaTypes) { // TODO should not be public and should not have nullable parameters this.mapper = mapper != null ? mapper : new ObjectMapper(); this.supportedMediaTypes = supportedMediaTypes != null ? supportedMediaTypes : DEFAULT_MEDIA_TYPES; } @Override public boolean canRead(final Class<?> clazz, final MediaType mediaType) { return this.canRead(clazz, null, mediaType); } @SuppressWarnings("deprecation") private JavaType getJavaType(final Type type, @Nullable final Class<?> contextClass) { final TypeFactory tf = mapper.getTypeFactory(); // Conditional call because Jackson 2.7 does not support null contextClass anymore // TypeVariable resolution will not work with Jackson 2.7, see SPR-13853 for more details return (contextClass != null) ? tf.constructType(type, contextClass) : tf.constructType(type); } private boolean canRead(@Nullable final MediaType mediaType) { return mediaType == null || getSupportedMediaTypes().stream().anyMatch(mediaType::isCompatibleWith); } @Override public boolean canRead(final Type type, @Nullable final Class<?> contextClass, final MediaType mediaType) { final JavaType javaType = this.getJavaType(type, contextClass); if (Stream.class.isAssignableFrom(javaType.getRawClass())) { final JavaType containedType = javaType.containedType(0); return (containedType != null) && mapper.canDeserialize(containedType) && canRead(mediaType); } return mapper.canDeserialize(javaType) && canRead(mediaType); } @Override public boolean canWrite(final Class<?> clazz, final MediaType mediaType) { return false; } // @Override since 4.2 public boolean canWrite(final Type type, final Class<?> clazz, final MediaType mediaType) { return false; } @Override public List<MediaType> getSupportedMediaTypes() { return Collections.unmodifiableList(supportedMediaTypes); } @Override public T read(final Class<? extends T> clazz, final HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { final JavaType javaType = getJavaType(clazz, null); return read(javaType, inputMessage); } @Override public T read(final Type type, final Class<?> contextClass, final HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { final JavaType javaType = getJavaType(type, contextClass); return read(javaType, inputMessage); } private T read(final JavaType javaType, final HttpInputMessage inputMessage) { try { if (Stream.class.isAssignableFrom(javaType.getRawClass())) { return stream(javaType.containedType(0), input(inputMessage)); } return mapper.readValue(inputMessage.getBody(), javaType); } catch (final IOException ex) { throw new HttpMessageNotReadableException("Could not read document: " + ex.getMessage(), ex); } } @SuppressWarnings("unchecked") private T stream(final JavaType javaType, final InputStream stream) throws IOException { final JsonParser parser = mapper.getFactory().createParser(stream); final StreamSpliterator<T> split = new StreamSpliterator<>(javaType, parser); return (T) StreamSupport.stream(split, false).onClose(() -> { try { parser.close(); } catch (final IOException e) { throw new UncheckedIOException(e); } }); } private InputStream input(final HttpInputMessage inputMessage) throws IOException { final MediaType contentType = inputMessage.getHeaders().getContentType(); final boolean sequence = APPLICATION_JSON_SEQ.includes(contentType); return sequence ? new StreamFilter(inputMessage.getBody()) : inputMessage.getBody(); } @Override public void write(final T t, final MediaType mediaType, final HttpOutputMessage message) { throw new UnsupportedOperationException(); } // @Override since 4.2 public void write(final T t, final Type type, final MediaType mediaType, final HttpOutputMessage message) { throw new UnsupportedOperationException(); } }