Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.inverno.mod.boot.converter.JacksonStringConverter Maven / Gradle / Ivy
/*
* Copyright 2022 Jeremy KUHN
*
* 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 io.inverno.mod.boot.converter;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonStreamContext;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.async.ByteArrayFeeder;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext;
import com.fasterxml.jackson.databind.util.TokenBuffer;
import io.inverno.core.annotation.Bean;
import io.inverno.core.annotation.Provide;
import io.inverno.mod.base.converter.Converter;
import io.inverno.mod.base.converter.ConverterException;
import io.inverno.mod.base.converter.JoinableEncoder;
import io.inverno.mod.base.converter.ReactiveConverter;
import io.inverno.mod.base.converter.SplittableDecoder;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.reactivestreams.Publisher;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
*
* JSON String to Object converter backed by an {@link ObjectMapper}.
*
*
* @author Jeremy Kuhn
* @since 1.5
*
* @see Converter
* @see ReactiveConverter
* @see ObjectMapper
*/
@Bean( name = "jsonStringConverter" )
public class JacksonStringConverter implements @Provide ReactiveConverter, SplittableDecoder, JoinableEncoder {
private static final String LAST_CHUNK = new String();
private static final Mono LAST_CHUNK_PUBLISHER = Mono.just(LAST_CHUNK);
private final ObjectMapper mapper;
/**
*
* Creates a JSON String converter.
*
*
* @param mapper a Jackson object mapper
*/
public JacksonStringConverter(ObjectMapper mapper) {
this.mapper = mapper;
}
@Override
public Publisher encodeOne(Mono value) {
return value.map(t -> this.encode(t));
}
@Override
public Publisher encodeOne(Mono value, Class type) {
return this.encodeOne(value, (Type)type);
}
@Override
public Publisher encodeOne(Mono value, Type type) {
return value.map(t -> this.encode(t, type));
}
@Override
public Publisher encodeMany(Flux value) {
return value.map(t -> this.encode(t));
}
@Override
public Publisher encodeMany(Flux value, Class type) {
return this.encodeMany(value, (Type)type);
}
@Override
public Publisher encodeMany(Flux value, Type type) {
return value.map(t -> this.encode(t, type));
}
@Override
public String encode(T value) throws ConverterException {
try {
return this.mapper.writeValueAsString(value);
}
catch (JsonProcessingException e) {
throw new ConverterException("Error encoding value", e);
}
}
@Override
public String encode(T value, Class type) throws ConverterException {
return this.encode(value, (Type)type);
}
@Override
public String encode(T value, Type type) throws ConverterException {
try {
return this.mapper.writerFor(this.mapper.constructType(type)).writeValueAsString(value);
}
catch (JsonProcessingException e) {
throw new ConverterException("Error encoding value", e);
}
}
@Override
public String encodeList(List value) throws ConverterException {
return this.encode(value);
}
@Override
public String encodeList(List value, Class type) throws ConverterException {
return this.encode(value);
}
@Override
public String encodeList(List value, Type type) throws ConverterException {
return this.encode(value);
}
@Override
public String encodeSet(Set value) throws ConverterException {
return this.encode(value);
}
@Override
public String encodeSet(Set value, Class type) throws ConverterException {
return this.encode(value);
}
@Override
public String encodeSet(Set value, Type type) throws ConverterException {
return this.encode(value);
}
@Override
public String encodeArray(T[] value) throws ConverterException {
return this.encode(value);
}
@Override
public String encodeArray(T[] value, Class type) throws ConverterException {
return this.encode(value);
}
@Override
public String encodeArray(T[] value, Type type) throws ConverterException {
return this.encode(value);
}
@Override
public Mono decodeOne(Publisher value, Class type) {
return this.decodeMany(value, type, false).single();
}
@Override
public Mono decodeOne(Publisher value, Type type) {
return this.decodeMany(value, type, false).single();
}
@Override
public Flux decodeMany(Publisher value, Class type) {
return this.decodeMany(value, type, true);
}
@Override
public Flux decodeMany(Publisher value, Type type) {
return this.decodeMany(value, type, true);
}
private Flux decodeMany(Publisher value, Type type, boolean scanRootArray) {
// Performance wise, this might not be ideal because creating a flux is resource consuming
// TODO assess performance and see whether it is interesting to optimize this
return Flux.concat(value, LAST_CHUNK_PUBLISHER).scanWith(
() -> {
try {
return new ObjectScanner(type, this.mapper, scanRootArray);
}
catch(IOException e) {
throw Exceptions.propagate(e);
}
},
(scanner, chunk) -> {
try {
// This is on purpose we want to compare the instance so we can differentiate it from an empty chunk
if(chunk == LAST_CHUNK) {
scanner.endOfInput();
}
else {
scanner.feedInput(chunk);
}
return scanner;
}
catch (IOException e) {
throw Exceptions.propagate(e);
}
}
)
.skip(1)
.concatMap(scanner -> {
try {
List objects = new LinkedList<>();
T object;
while( (object = scanner.nextObject()) != null) {
objects.add(object);
}
return Flux.fromIterable(objects);
}
catch (IOException e) {
throw Exceptions.propagate(e);
}
});
}
@Override
public T decode(String value, Class type) throws ConverterException {
return this.decode(value, (Type)type);
}
@Override
public T decode(String value, Type type) throws ConverterException {
try {
return this.mapper.readerFor(this.mapper.constructType(type)).readValue(value);
}
catch (IOException e) {
throw new ConverterException("Error decoding value", e);
}
}
@Override
public List decodeToList(String value, Class type) {
return this.decodeToList(value, (Type)type);
}
@Override
public List decodeToList(String value, Type type) {
try {
ObjectScanner scanner = new ObjectScanner<>(type, this.mapper, true);
scanner.feedInput(value);
scanner.endOfInput();
List objects = new LinkedList<>();
T object;
while( (object = scanner.nextObject()) != null) {
objects.add(object);
}
return objects;
}
catch (IOException e) {
throw new ConverterException("Error decoding value", e);
}
}
@Override
public Set decodeToSet(String value, Class type) {
return this.decodeToSet(value, (Type)type);
}
@Override
public Set decodeToSet(String value, Type type) {
try {
ObjectScanner scanner = new ObjectScanner<>(type, this.mapper, true);
scanner.feedInput(value);
scanner.endOfInput();
Set objects = new HashSet<>();
T object;
while( (object = scanner.nextObject()) != null) {
objects.add(object);
}
return objects;
}
catch (IOException e) {
throw new ConverterException("Error decoding value", e);
}
}
@SuppressWarnings("unchecked")
@Override
public T[] decodeToArray(String value, Class type) {
List objects = this.decodeToList(value, type);
return objects.toArray((T[])Array.newInstance(type, objects.size()));
}
@SuppressWarnings("unchecked")
@Override
public T[] decodeToArray(String value, Type type) {
List objects = this.decodeToList(value, type);
if(type instanceof Class) {
return objects.toArray((T[]) Array.newInstance((Class)type, objects.size()));
}
else if(type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType)type;
return objects.toArray((T[]) Array.newInstance((Class)parameterizedType.getRawType(), objects.size()));
}
else {
throw new ConverterException("Can't decode " + String.class.getCanonicalName() + " to array of " + type.getTypeName());
}
}
private static class ObjectScanner {
private final Type type;
private final ObjectMapper mapper;
private final ObjectReader reader;
private final boolean scanRootArray;
private final JsonParser parser;
private final ByteArrayFeeder feeder;
private DeserializationContext deserializationContext;
private TokenBuffer tokenBuffer;
public ObjectScanner(Type type, ObjectMapper mapper, boolean scanRootArray) throws IOException {
this.type = type;
this.mapper = mapper;
this.reader = this.mapper.readerFor(this.mapper.constructType(this.type));
this.scanRootArray = scanRootArray;
this.parser = this.mapper.getFactory().createNonBlockingByteArrayParser();
this.feeder = (ByteArrayFeeder)this.parser.getNonBlockingInputFeeder();
this.deserializationContext = this.mapper.getDeserializationContext();
if (this.deserializationContext instanceof DefaultDeserializationContext) {
this.deserializationContext = ((DefaultDeserializationContext) this.deserializationContext).createInstance(this.mapper.getDeserializationConfig(), this.parser, this.mapper.getInjectableValues());
}
}
@SuppressWarnings("unused")
public ObjectScanner(Class type, ObjectMapper mapper, boolean scanRootArray) throws IOException {
this((Type)type, mapper, scanRootArray);
}
private TokenBuffer getTokenBuffer() {
if(this.tokenBuffer == null) {
this.tokenBuffer = new TokenBuffer(this.parser, this.deserializationContext);
this.tokenBuffer.forceUseOfBigDecimal(this.mapper.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS));
}
return this.tokenBuffer;
}
public void feedInput(String chunk) throws IOException {
byte[] bytes = chunk.getBytes();
this.feeder.feedInput(bytes, 0, bytes.length);
}
public void endOfInput() {
this.feeder.endOfInput();
}
public T nextObject() throws IOException {
while (!this.parser.isClosed()) {
JsonToken token = this.parser.nextToken();
// TODO smile value format uses null to separate document
// we actually know in advanced that we are dealing with that format so maybe we can provide another scanner implementation to make things explicit
if(token == null || token == JsonToken.NOT_AVAILABLE) {
// end of input
break;
}
JsonStreamContext context = this.parser.getParsingContext();
TokenBuffer currentTokenBuffer = this.getTokenBuffer();
if(this.scanRootArray && context.inArray() && context.getParent().inRoot() && (token == JsonToken.START_ARRAY || token == JsonToken.END_ARRAY)) {
continue;
}
currentTokenBuffer.copyCurrentEvent(this.parser);
if( (context.inRoot() || (this.scanRootArray && context.inArray() && context.getParent().inRoot())) && (token.isScalarValue() || token.isStructEnd())) {
try {
return this.reader.readValue(currentTokenBuffer.asParser());
}
finally {
this.tokenBuffer = null;
}
}
}
return null;
}
}
}