Skip to content

Commit 276171b

Browse files
committed
JOHNZON-421 Defer start of object/array
This is necessary as we don't know what Serializers/Adapters/Converters actually do write. If we start an object with { and the JsonbSerializer just writes a string value (json primitive) then we will blow up. Deferring the start fixes this issue.
1 parent 8acca1f commit 276171b

16 files changed

Lines changed: 651 additions & 130 deletions

File tree

johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,9 @@ public byte[] fromString(final String text) {
309309
throw new IllegalArgumentException("We only support serializer on Class for now");
310310
}
311311
builder.addObjectConverter(
312-
Class.class.cast(args[0]), (ObjectConverter.Writer) (instance, jsonbGenerator) ->
312+
Class.class.cast(args[0]), (ObjectConverter.Writer) (instance, jsonbGenerator, generator) ->
313313
s.serialize(
314-
instance, jsonbGenerator.getJsonGenerator(),
314+
instance, generator,
315315
new JohnzonSerializationContext(jsonbGenerator)));
316316
});
317317
});

johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,8 @@ final boolean record = isRecord(clazz) || Meta.getAnnotation(clazz, JohnzonRecor
264264
final JsonbDateFormat dateFormat = getAnnotation(parameter, JsonbDateFormat.class);
265265
final JsonbNumberFormat numberFormat = getAnnotation(parameter, JsonbNumberFormat.class);
266266
final JohnzonConverter johnzonConverter = getAnnotation(parameter, JohnzonConverter.class);
267-
if (adapter == null && dateFormat == null && numberFormat == null && johnzonConverter == null) {
267+
final JsonbTypeDeserializer deserializer = getAnnotation(parameter, JsonbTypeDeserializer.class);
268+
if (adapter == null && dateFormat == null && numberFormat == null && johnzonConverter == null && deserializer == null) {
268269
converters[i] = defaultConverters.get(parameter.getType());
269270
itemConverters[i] = null;
270271
} else {
@@ -283,6 +284,63 @@ final boolean record = isRecord(clazz) || Meta.getAnnotation(clazz, JohnzonRecor
283284
}
284285
} else if (johnzonConverter != null) {
285286
objectConverters[i] = (ObjectConverter.Codec<?>) johnzonConverter.value().newInstance();
287+
288+
} else if (deserializer != null) {
289+
final Class<? extends JsonbDeserializer> value = deserializer.value();
290+
final JohnzonAdapterFactory.Instance<? extends JsonbDeserializer> instance = newInstance(value);
291+
final ParameterizedType pt = this.types.findParameterizedType(value, JsonbDeserializer.class);
292+
final Class<?> mappedType = this.types.findParamType(pt, JsonbDeserializer.class);
293+
toRelease.add(instance);
294+
final JsonBuilderFactory builderFactoryInstance = builderFactory.get();
295+
final Type[] arguments = this.types.findParameterizedType(value, JsonbDeserializer.class).getActualTypeArguments();
296+
final boolean global = arguments.length == 1 && arguments[0] != null && arguments[0].equals(parameter.getType());
297+
objectConverters[i] = new ObjectConverter.Codec() {
298+
private final ConcurrentMap<Type, BiFunction<JsonValue, MappingParser, Object>> impl =
299+
new ConcurrentHashMap<>();
300+
@Override
301+
public Object fromJson(final JsonValue value, final Type targetType, final MappingParser parser) {
302+
final JsonbDeserializer jsonbDeserializer = instance.getValue();
303+
if (global || targetType == mappedType) { // fast test and matches most cases
304+
return mapItem(value, targetType, parser, jsonbDeserializer);
305+
}
306+
307+
BiFunction<JsonValue, MappingParser, Object> fn = impl.get(targetType);
308+
if (fn == null) {
309+
if (value.getValueType() == JsonValue.ValueType.ARRAY) {
310+
if (ParameterizedType.class.isInstance(targetType)) {
311+
final ParameterizedType parameterizedType = ParameterizedType.class.cast(targetType);
312+
final Class<?> paramType = JsonbAccessMode.this.types.findParamType(parameterizedType, Collection.class);
313+
if (paramType != null && (mappedType == null /*Object*/ || mappedType.isAssignableFrom(paramType))) {
314+
final Collector<Object, ?, ? extends Collection<Object>> collector =
315+
Set.class.isAssignableFrom(
316+
JsonbAccessMode.this.types.asClass(parameterizedType.getRawType())) ? toSet() : toList();
317+
fn = (json, mp) -> json.asJsonArray().stream()
318+
.map(i -> mapItem(i, paramType, mp, jsonbDeserializer))
319+
.collect(collector);
320+
}
321+
}
322+
}
323+
if (fn == null) {
324+
fn = (json, mp) -> mapItem(json, targetType, mp, jsonbDeserializer);
325+
}
326+
impl.putIfAbsent(targetType, fn);
327+
}
328+
return fn.apply(value, parser);
329+
}
330+
331+
private Object mapItem(final JsonValue jsonValue, final Type targetType,
332+
final MappingParser parser, final JsonbDeserializer jsonbDeserializer) {
333+
return jsonbDeserializer.deserialize(
334+
JsonValueParserAdapter.createFor(jsonValue, parserFactory),
335+
new JohnzonDeserializationContext(parser, builderFactoryInstance, jsonProvider),
336+
targetType);
337+
}
338+
339+
@Override
340+
public void writeJson(final Object instance, final MappingGenerator jsonbGenerator, JsonGenerator generator) {
341+
// no-op, it's for factories only
342+
}
343+
};
286344
}
287345
} catch (final InstantiationException | IllegalAccessException e) {
288346
throw new IllegalArgumentException(e);
@@ -607,8 +665,9 @@ public boolean isNillable(final boolean globalConfig) {
607665

608666
final WriterConverters writerConverters = new WriterConverters(annotations, types);
609667
final JsonbProperty property = annotations.getAnnotation(JsonbProperty.class);
610-
final JsonbNillable nillable = annotations.getClassOrPackageAnnotation(JsonbNillable.class);
611-
final boolean isNillable = isNillable(property, nillable);
668+
final JsonbNillable propertyNillable = annotations.getAnnotation(JsonbNillable.class);
669+
final JsonbNillable classOrPackageNillable = annotations.getClassOrPackageAnnotation(JsonbNillable.class);
670+
final boolean isNillable = isNillable(property, propertyNillable, classOrPackageNillable);
612671
final String key = property == null || property.value().isEmpty() ? naming.translateName(entry.getKey()) : property.value();
613672
if (result.put(key, new Reader() {
614673
@Override
@@ -725,8 +784,9 @@ public Map<String, Writer> findWriters(final Class<?> clazz) {
725784

726785
final ReaderConverters converters = new ReaderConverters(initialWriter);
727786
final JsonbProperty property = initialWriter.getAnnotation(JsonbProperty.class);
728-
final JsonbNillable nillable = initialWriter.getClassOrPackageAnnotation(JsonbNillable.class);
729-
final boolean isNillable = isNillable(property, nillable);
787+
final JsonbNillable propertyNillable = initialWriter.getAnnotation(JsonbNillable.class);
788+
final JsonbNillable classOrPackageNillable = initialWriter.getClassOrPackageAnnotation(JsonbNillable.class);
789+
final boolean isNillable = isNillable(property, propertyNillable, classOrPackageNillable);
730790
final String key = property == null || property.value().isEmpty() ? naming.translateName(entry.getKey()) : property.value();
731791
if (result.put(key, new Writer() {
732792
@Override
@@ -829,12 +889,14 @@ private boolean isReversedAdapter(final Class<?> payloadType, final Class<?> aCl
829889
});
830890
}
831891

832-
private boolean isNillable(final JsonbProperty property, final JsonbNillable nillable) {
833-
if (property != null) {
892+
private boolean isNillable(final JsonbProperty property, final JsonbNillable propertyNillable, final JsonbNillable classOrPackageNillable) {
893+
if (propertyNillable != null) {
894+
return propertyNillable.value();
895+
} else if (property != null) {
834896
return property.nillable();
835897
}
836-
if (nillable != null) {
837-
return nillable.value();
898+
if (classOrPackageNillable != null) {
899+
return classOrPackageNillable.value();
838900
}
839901
return globalIsNillable;
840902
}
@@ -1121,8 +1183,7 @@ private class WriterConverters {
11211183
final JsonbSerializer jsonbSerializer = instance.getValue();
11221184
writer = new ObjectConverter.Writer() {
11231185
@Override
1124-
public void writeJson(final Object instance, final MappingGenerator jsonbGenerator) {
1125-
final JsonGenerator generator = jsonbGenerator.getJsonGenerator();
1186+
public void writeJson(final Object instance, final MappingGenerator jsonbGenerator, JsonGenerator generator) {
11261187
jsonbSerializer.serialize(instance, generator, new JohnzonSerializationContext(jsonbGenerator));
11271188
}
11281189

johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JohnzonConverterInJsonbTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import javax.json.JsonValue;
2828
import javax.json.bind.Jsonb;
2929
import javax.json.bind.spi.JsonbProvider;
30+
import javax.json.stream.JsonGenerator;
3031

3132
import org.apache.johnzon.mapper.Converter;
3233
import org.apache.johnzon.mapper.JohnzonConverter;
@@ -88,8 +89,8 @@ public static class TestDTOConverter implements ObjectConverter.Codec<TestDTO> {
8889
private static final String TIMESTAMP_JSON_KEY = "timestamp";
8990

9091
@Override
91-
public void writeJson(TestDTO instance, MappingGenerator jsonbGenerator) {
92-
jsonbGenerator.getJsonGenerator().write(TIMESTAMP_JSON_KEY, instance.instant.atZone(ZoneId.of("UTC")).toString());
92+
public void writeJson(TestDTO instance, MappingGenerator jsonbGenerator, JsonGenerator generator) {
93+
generator.write(TIMESTAMP_JSON_KEY, instance.instant.atZone(ZoneId.of("UTC")).toString());
9394
}
9495

9596
@Override

johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/SerializerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public void passthroughSerializer() {
6969
nameHolder.name.detailName = new DetailName();
7070
nameHolder.name.detailName.name = "Another Test String";
7171
assertEquals(
72-
"{\"detailName\":{\"name\":\"Another Test String\",\"detail\":true},\"name\":{\"name\":\"Test String\"}}",
72+
"{\"name\":{\"detailName\":{\"name\":\"Another Test String\",\"detail\":true},\"name\":\"Test String\"}}",
7373
jsonb.toJson(nameHolder));
7474

7575
}

johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/SerializersRoundTripTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,8 @@ public void roundTrip() throws Exception {
564564
original.color = Color.GREEN;*/
565565

566566
try (final Jsonb jsonb = JsonbBuilder.create()) {
567-
final Wrapper deserialized = jsonb.fromJson(jsonb.toJson(original), Wrapper.class);
567+
final String json = jsonb.toJson(original);
568+
final Wrapper deserialized = jsonb.fromJson(json, Wrapper.class);
568569
assertEquals(original, deserialized);
569570
}
570571
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.johnzon.jsonb.serializer;
18+
19+
import java.lang.reflect.Type;
20+
21+
import org.junit.Test;
22+
23+
import javax.json.bind.Jsonb;
24+
import javax.json.bind.JsonbBuilder;
25+
import javax.json.bind.annotation.JsonbTypeDeserializer;
26+
import javax.json.bind.annotation.JsonbTypeSerializer;
27+
import javax.json.bind.serializer.DeserializationContext;
28+
import javax.json.bind.serializer.JsonbDeserializer;
29+
import javax.json.bind.serializer.JsonbSerializer;
30+
import javax.json.bind.serializer.SerializationContext;
31+
import javax.json.stream.JsonGenerator;
32+
import javax.json.stream.JsonParser;
33+
import static org.junit.Assert.assertTrue;
34+
35+
/**
36+
* This test checks a JsonbSerialize/JsonbDeserialize roundtrip when using a primitive as placeholder
37+
*/
38+
public class SerialiseAsPrimitiveAnnotatedAtClassTest {
39+
40+
41+
@JsonbTypeSerializer(ConstantJsonbSerialiser.class)
42+
@JsonbTypeDeserializer(ConstantJsonbDeserializer.class)
43+
public static class TestConstant {
44+
public final static TestConstant VAL_1 = new TestConstant("A");
45+
public final static TestConstant VAL_2 = new TestConstant("B");
46+
public final static TestConstant VAL_3 = new TestConstant("C");
47+
48+
private final String code;
49+
50+
public TestConstant(String code) {
51+
this.code = code;
52+
}
53+
54+
public String getCode() {
55+
return code;
56+
}
57+
58+
public static TestConstant getByCode(String code) {
59+
switch (code) {
60+
case "A": return VAL_1;
61+
case "B": return VAL_2;
62+
case "C": return VAL_3;
63+
default: return null;
64+
}
65+
}
66+
}
67+
68+
public static class ConstantUsage {
69+
private int i;
70+
71+
private TestConstant testConstant;
72+
73+
public int getI() {
74+
return i;
75+
}
76+
77+
public void setI(int i) {
78+
this.i = i;
79+
}
80+
81+
public TestConstant getTestConstant() {
82+
return testConstant;
83+
}
84+
85+
public void setTestConstant(TestConstant testEnum) {
86+
this.testConstant = testEnum;
87+
}
88+
}
89+
90+
public static class ConstantJsonbSerialiser implements JsonbSerializer<TestConstant> {
91+
@Override
92+
public void serialize(TestConstant val, JsonGenerator generator, SerializationContext ctx) {
93+
if (val == null) {
94+
ctx.serialize(null, generator);
95+
} else {
96+
ctx.serialize(val.getCode(), generator);
97+
}
98+
}
99+
}
100+
101+
public static class ConstantJsonbDeserializer implements JsonbDeserializer<TestConstant> {
102+
103+
@Override
104+
public TestConstant deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
105+
if (rtType instanceof TestConstant) {
106+
final String key = parser.getString();
107+
if (key != null) {
108+
return TestConstant.getByCode(key);
109+
}
110+
}
111+
112+
return null;
113+
}
114+
}
115+
116+
117+
118+
@Test
119+
public void testEnumJsonb() {
120+
ConstantUsage enumVerwender = new ConstantUsage();
121+
enumVerwender.setI(1);
122+
enumVerwender.setTestConstant(TestConstant.VAL_2);
123+
124+
Jsonb jsonb = JsonbBuilder.create();
125+
final String json = jsonb.toJson(enumVerwender);
126+
assertTrue(json.contains("\"testConstant\":\"B\""));
127+
}
128+
129+
}

johnzon-mapper/src/main/java/org/apache/johnzon/mapper/DynamicMappingGenerator.java

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.math.BigDecimal;
2424
import java.math.BigInteger;
2525

26+
@Deprecated
2627
public class DynamicMappingGenerator implements MappingGenerator {
2728
private final MappingGenerator delegate;
2829
private final Runnable writeStart;
@@ -40,33 +41,16 @@ public DynamicMappingGenerator(final MappingGenerator delegate,
4041
this.writeEnd = writeEnd;
4142
this.keyName = keyName;
4243
}
43-
44-
protected JsonGenerator getRawJsonGenerator() {
45-
return delegate.getJsonGenerator();
46-
}
47-
48-
@Override
49-
public JsonGenerator getJsonGenerator() {
50-
return generator == null ? generator = new InObjectOrPrimitiveJsonGenerator(
51-
getRawJsonGenerator(), writeStart, keyName, writeEnd) : generator;
52-
}
44+
5345

5446
@Override
5547
public MappingGenerator writeObject(final String key, final Object o, final JsonGenerator generator) {
56-
return delegate.writeObject(key, o, ensureGenerator(generator));
48+
return delegate.writeObject(key, o, generator);
5749
}
5850

5951
@Override
6052
public MappingGenerator writeObject(final Object o, final JsonGenerator generator) {
61-
return delegate.writeObject(o, ensureGenerator(generator));
62-
}
63-
64-
private JsonGenerator ensureGenerator(final JsonGenerator generator) {
65-
if (this.generator != null && this.generator != generator && this.generator.delegate != generator) {
66-
this.generator = null;
67-
reset();
68-
}
69-
return getJsonGenerator(); // ensure we wrap it
53+
return delegate.writeObject(o, generator);
7054
}
7155

7256
protected void reset() {
@@ -593,24 +577,5 @@ public SkipEnclosingWriteEnd(final MappingGenerator delegate, final String keyNa
593577
super(delegate, NOOP, NOOP, keyName);
594578
this.rawGenerator = generator;
595579
}
596-
597-
@Override
598-
protected JsonGenerator getRawJsonGenerator() {
599-
return rawGenerator;
600-
}
601-
602-
@Override
603-
public JsonGenerator getJsonGenerator() {
604-
if (skippingGenerator == null) {
605-
skippingGenerator = new SkipLastWriteEndGenerator(super.getJsonGenerator());
606-
}
607-
return skippingGenerator;
608-
}
609-
610-
@Override
611-
protected void reset() {
612-
super.reset();
613-
skippingGenerator = null;
614-
}
615580
}
616581
}

johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ public void writeObject(final Object object, final OutputStream stream) {
203203

204204
private void writeObject(final Object object, final JsonGenerator generator, final Collection<String> ignored,
205205
final JsonPointerTracker tracker) {
206-
final MappingGeneratorImpl mappingGenerator = new MappingGeneratorImpl(config, generator, mappings);
206+
final MappingGeneratorImpl mappingGenerator = new MappingGeneratorImpl(config, mappings);
207207
mappingGenerator.doWriteObject(object, generator, true, ignored, tracker);
208208
}
209209

0 commit comments

Comments
 (0)