| /* |
| * 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.apache.johnzon.jsonb; |
| |
| import org.apache.johnzon.core.AbstractJsonFactory; |
| import org.apache.johnzon.core.JsonGeneratorFactoryImpl; |
| import org.apache.johnzon.core.JsonParserFactoryImpl; |
| import org.apache.johnzon.core.Types; |
| import org.apache.johnzon.jsonb.adapter.JsonbEnumAdapter; |
| import org.apache.johnzon.jsonb.api.experimental.PolymorphicConfig; |
| import org.apache.johnzon.jsonb.cdi.CDIs; |
| import org.apache.johnzon.jsonb.converter.JohnzonJsonbAdapter; |
| import org.apache.johnzon.jsonb.factory.SimpleJohnzonAdapterFactory; |
| import org.apache.johnzon.jsonb.serializer.JohnzonDeserializationContext; |
| import org.apache.johnzon.jsonb.serializer.JohnzonSerializationContext; |
| import org.apache.johnzon.jsonb.spi.JohnzonAdapterFactory; |
| import org.apache.johnzon.mapper.Converter; |
| import org.apache.johnzon.mapper.Mapper; |
| import org.apache.johnzon.mapper.MapperBuilder; |
| import org.apache.johnzon.mapper.MapperConfig; |
| import org.apache.johnzon.mapper.ObjectConverter; |
| import org.apache.johnzon.mapper.SerializeValueFilter; |
| import org.apache.johnzon.mapper.access.AccessMode; |
| import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode; |
| import org.apache.johnzon.mapper.access.KnownNotOpenedJavaTypesAccessMode; |
| import org.apache.johnzon.mapper.converter.LocaleConverter; |
| import org.apache.johnzon.mapper.internal.AdapterKey; |
| |
| import javax.json.JsonBuilderFactory; |
| import javax.json.bind.Jsonb; |
| import javax.json.bind.JsonbBuilder; |
| import javax.json.bind.JsonbConfig; |
| import javax.json.bind.adapter.JsonbAdapter; |
| import javax.json.bind.config.BinaryDataStrategy; |
| import javax.json.bind.config.PropertyNamingStrategy; |
| import javax.json.bind.config.PropertyVisibilityStrategy; |
| import javax.json.bind.serializer.JsonbDeserializer; |
| import javax.json.bind.serializer.JsonbSerializer; |
| import javax.json.spi.JsonProvider; |
| import javax.json.stream.JsonGenerator; |
| import javax.json.stream.JsonParserFactory; |
| import java.io.Closeable; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.ParameterizedType; |
| import java.lang.reflect.Type; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Base64; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.function.Supplier; |
| import java.util.stream.Stream; |
| |
| import static java.time.format.DateTimeFormatter.ofPattern; |
| import static java.util.Collections.emptyMap; |
| import static java.util.Optional.ofNullable; |
| import static javax.json.bind.config.PropertyNamingStrategy.IDENTITY; |
| import static javax.json.bind.config.PropertyOrderStrategy.ANY; |
| |
| public class JohnzonBuilder implements JsonbBuilder { |
| private static final Object NO_BM = new Object(); |
| |
| private final MapperBuilder builder = new MapperBuilder(); |
| private JsonProvider jsonp; |
| private JsonbConfig config; |
| private Object beanManager; |
| private CDIs cdiIntegration; |
| |
| @Override |
| public JsonbBuilder withConfig(final JsonbConfig config) { |
| this.config = config; |
| return this; |
| } |
| |
| @Override |
| public JsonbBuilder withProvider(final JsonProvider jsonpProvider) { |
| this.jsonp = jsonpProvider; |
| return this; |
| } |
| |
| @Override |
| public Jsonb build() { |
| builder.setEnumConverterFactory(type -> newEnumConverter(Class.class.cast(type))); |
| if (jsonp != null) { |
| builder.setGeneratorFactory(jsonp.createGeneratorFactory(generatorConfig())); |
| builder.setReaderFactory(jsonp.createReaderFactory(readerConfig())); |
| } else { |
| jsonp = JsonProvider.provider(); |
| } |
| final Supplier<JsonBuilderFactory> builderFactorySupplier = createJsonBuilderFactory(); |
| final Supplier<JsonParserFactory> parserFactoryProvider = createJsonParserFactory(); |
| |
| if (config == null) { |
| config = new JsonbConfig(); |
| } |
| |
| final boolean skipCdi = shouldSkipCdi(); |
| |
| // todo: global spec toggle to disable all these ones at once? |
| builder.setUseBigDecimalForObjectNumbers( |
| config.getProperty("johnzon.use-big-decimal-for-object").map(this::toBool).orElse(true)); |
| builder.setSupportEnumContainerDeserialization( // https://github.com/eclipse-ee4j/jakartaee-tck/issues/103 |
| toBool(System.getProperty("johnzon.support-enum-container-deserialization", config.getProperty("johnzon.support-enum-container-deserialization") |
| .map(String::valueOf).orElse("true")))); |
| |
| final boolean ijson = config.getProperty(JsonbConfig.STRICT_IJSON) |
| .map(Boolean.class::cast) |
| .filter(it -> it) |
| .map(it -> { |
| if (!config.getProperty(JsonbConfig.BINARY_DATA_STRATEGY).isPresent()) { |
| config.withBinaryDataStrategy(BinaryDataStrategy.BASE_64); |
| } |
| if (!config.getProperty(JsonbConfig.DATE_FORMAT).isPresent()) { |
| config.withDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'xxx", Locale.ROOT); |
| } |
| return it; |
| }).orElse(false); |
| |
| if (config.getProperty(JsonbConfig.FORMATTING).map(Boolean.class::cast).orElse(false)) { |
| builder.setPretty(true); |
| } |
| |
| config.getProperty(PolymorphicConfig.class.getName()) |
| .map(PolymorphicConfig.class::cast) |
| .ifPresent(pc -> { |
| builder.setPolymorphicDiscriminator(pc.getDiscriminator()); |
| builder.setPolymorphicDeserializationPredicate(pc.getDeserializationPredicate()); |
| builder.setPolymorphicSerializationPredicate(pc.getSerializationPredicate()); |
| builder.setPolymorphicDiscriminatorMapper(pc.getDiscriminatorMapper()); |
| builder.setPolymorphicTypeLoader(pc.getTypeLoader()); |
| }); |
| config.getProperty(JsonbConfig.ENCODING).ifPresent(encoding -> builder.setEncoding(String.valueOf(encoding))); |
| final boolean isNillable = config.getProperty(JsonbConfig.NULL_VALUES) |
| .map(it -> String.class.isInstance(it) ? Boolean.parseBoolean(it.toString()) : Boolean.class.cast(it)) |
| .map(serNulls -> { |
| builder.setSkipNull(!serNulls); |
| return serNulls; |
| }) |
| .orElse(false); |
| |
| final Optional<Object> namingStrategyValue = config.getProperty(JsonbConfig.PROPERTY_NAMING_STRATEGY); |
| |
| final PropertyNamingStrategy propertyNamingStrategy = new PropertyNamingStrategyFactory(namingStrategyValue.orElse(IDENTITY)).create(); |
| final String orderValue = config.getProperty(JsonbConfig.PROPERTY_ORDER_STRATEGY).map(String::valueOf).orElse(ANY); |
| final PropertyVisibilityStrategy visibilityStrategy = config.getProperty(JsonbConfig.PROPERTY_VISIBILITY_STRATEGY) |
| .map(PropertyVisibilityStrategy.class::cast).orElse(new DefaultPropertyVisibilityStrategy()); |
| |
| config.getProperty("johnzon.attributeOrder").ifPresent(comp -> builder.setAttributeOrder(Comparator.class.cast(comp))); |
| config.getProperty("johnzon.enforceQuoteString") |
| .map(this::toBool) |
| .ifPresent(builder::setEnforceQuoteString); |
| config.getProperty("johnzon.primitiveConverters") |
| .map(this::toBool) |
| .ifPresent(builder::setPrimitiveConverters); |
| config.getProperty("johnzon.useBigDecimalForFloats") |
| .map(this::toBool) |
| .ifPresent(builder::setUseBigDecimalForFloats); |
| config.getProperty("johnzon.deduplicateObjects") |
| .map(this::toBool) |
| .ifPresent(builder::setDeduplicateObjects); |
| config.getProperty("johnzon.interfaceImplementationMapping") |
| .map(Map.class::cast) |
| .ifPresent(builder::setInterfaceImplementationMapping); |
| builder.setUseJsRange(toBool( // https://github.com/eclipse-ee4j/jsonb-api/issues/180 |
| System.getProperty("johnzon.use-js-range", config.getProperty("johnzon.use-js-range") |
| .map(String::valueOf).orElse("false")))); |
| builder.setUseShortISO8601Format(false); |
| config.getProperty(JsonbConfig.DATE_FORMAT) |
| .map(String.class::cast) |
| .ifPresent(dateFormat -> builder.setAdaptersDateTimeFormatter(config.getProperty(JsonbConfig.LOCALE) |
| .map(it -> String.class.isInstance(it) ? new LocaleConverter().to(it.toString()) : Locale.class.cast(it)) |
| .map(value -> ofPattern(dateFormat, value)) |
| .orElseGet(() -> ofPattern(dateFormat)))); |
| |
| final JohnzonAdapterFactory factory = config.getProperty("johnzon.factory").map(val -> { |
| if (JohnzonAdapterFactory.class.isInstance(val)) { |
| return JohnzonAdapterFactory.class.cast(val); |
| } |
| if (String.class.isInstance(val)) { |
| try { |
| return JohnzonAdapterFactory.class.cast(tccl().loadClass(val.toString()).newInstance()); |
| } catch (final InstantiationException | ClassNotFoundException | IllegalAccessException e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| if (Class.class.isInstance(val)) { |
| try { |
| return JohnzonAdapterFactory.class.cast(Class.class.cast(val).newInstance()); |
| } catch (final InstantiationException | IllegalAccessException e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| throw new IllegalArgumentException("Unsupported factory: " + val); |
| }).orElseGet(() -> findFactory(skipCdi)); |
| |
| final AccessMode accessMode = config.getProperty("johnzon.accessMode") |
| .map(this::toAccessMode) |
| .orElseGet(() -> { |
| final AccessMode access = new JsonbAccessMode( |
| propertyNamingStrategy, orderValue, visibilityStrategy, |
| !namingStrategyValue.orElse("").equals(PropertyNamingStrategy.CASE_INSENSITIVE), |
| builder.getAdapters(), |
| factory, jsonp, builderFactorySupplier, parserFactoryProvider, |
| config.getProperty("johnzon.accessModeDelegate") |
| .map(this::toAccessMode) |
| .orElseGet(() -> new FieldAndMethodAccessMode(true, true, false, true)), |
| config.getProperty("johnzon.failOnMissingCreatorValues") |
| .map(this::toBool) |
| .orElse(true) /*spec 1.0 requirement*/, |
| isNillable, |
| config.getProperty("johnzon.supportsPrivateAccess") |
| .map(this::toBool) |
| .orElse(false)); |
| return ofNullable(config.getProperty("johnzon.enable-exception-serialization")) |
| .map(v -> Boolean.parseBoolean(String.valueOf(v))) |
| .map(it -> it ? new KnownNotOpenedJavaTypesAccessMode(access) : access) |
| .orElseGet(() -> new KnownNotOpenedJavaTypesAccessMode(access)); |
| }); |
| builder.setAccessMode(accessMode); |
| |
| // user adapters |
| config.getProperty(JsonbConfig.ADAPTERS).ifPresent(adapters -> Stream.of(JsonbAdapter[].class.cast(adapters)).forEach(adapter -> { |
| final ParameterizedType pt = ParameterizedType.class.cast( |
| Stream.of(adapter.getClass().getGenericInterfaces()) |
| .filter(i -> ParameterizedType.class.isInstance(i) && ParameterizedType.class.cast(i).getRawType() == JsonbAdapter.class).findFirst().orElse(null)); |
| if (pt == null) { |
| throw new IllegalArgumentException(adapter + " doesn't implement JsonbAdapter"); |
| } |
| final Type[] args = pt.getActualTypeArguments(); |
| final JohnzonJsonbAdapter johnzonJsonbAdapter = new JohnzonJsonbAdapter(adapter, args[0], args[1]); |
| builder.addAdapter(args[0], args[1], johnzonJsonbAdapter); |
| builder.getAdapters().put(new AdapterKey(args[0], args[1]), johnzonJsonbAdapter); |
| })); |
| |
| ofNullable(config.getProperty("johnzon.fail-on-unknown-properties") |
| .orElseGet(() -> config.getProperty("jsonb.fail-on-unknown-properties").orElse(null))) |
| .map(v -> Boolean.class.isInstance(v) ? Boolean.class.cast(v) : Boolean.parseBoolean(String.valueOf(v))) |
| .ifPresent(builder::setFailOnUnknownProperties); |
| |
| config.getProperty(JsonbConfig.BINARY_DATA_STRATEGY).map(String.class::cast).ifPresent(bin -> { |
| switch (bin) { |
| case BinaryDataStrategy.BYTE: |
| // no-op: our default |
| break; |
| case BinaryDataStrategy.BASE_64: |
| builder.setTreatByteArrayAsBase64(true); |
| break; |
| case BinaryDataStrategy.BASE_64_URL: // needs j8 |
| builder.addConverter(byte[].class, new Converter<byte[]>() { |
| @Override |
| public String toString(final byte[] instance) { |
| return Base64.getUrlEncoder().encodeToString(instance); |
| } |
| |
| @Override |
| public byte[] fromString(final String text) { |
| return Base64.getUrlDecoder().decode(text.getBytes(StandardCharsets.UTF_8)); |
| } |
| }); |
| break; |
| default: |
| throw new IllegalArgumentException("Unsupported binary configuration: " + bin); |
| } |
| }); |
| |
| if (!skipCdi) { |
| getBeanManager(); // force detection |
| } |
| |
| final Types types = new Types(); |
| builder.setReadAttributeBeforeWrite( |
| config.getProperty("johnzon.readAttributeBeforeWrite").map(Boolean.class::cast).orElse(false)); |
| builder.setAutoAdjustStringBuffers( |
| config.getProperty("johnzon.autoAdjustBuffer").map(Boolean.class::cast).orElse(true)); |
| config.getProperty("johnzon.serialize-value-filter") |
| .map(s -> { |
| if (String.class.isInstance(s)) { |
| try { |
| return SerializeValueFilter.class.cast( |
| Thread.currentThread().getContextClassLoader().loadClass(s.toString()).getConstructor().newInstance()); |
| } catch (final InstantiationException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) { |
| throw new IllegalArgumentException(e); |
| } catch (InvocationTargetException e) { |
| throw new IllegalArgumentException(e.getCause()); |
| } |
| } |
| return s; |
| }) |
| .ifPresent(s -> builder.setSerializeValueFilter(SerializeValueFilter.class.cast(s))); |
| |
| config.getProperty(JsonbConfig.SERIALIZERS).map(JsonbSerializer[].class::cast).ifPresent(serializers -> { |
| Stream.of(serializers).forEach(s -> { |
| final ParameterizedType pt = types.findParameterizedType(s.getClass(), JsonbSerializer.class); |
| final Type[] args = pt.getActualTypeArguments(); |
| // TODO: support PT in ObjectConverter (list) |
| if (args.length != 1 || !Class.class.isInstance(args[0])) { |
| throw new IllegalArgumentException("We only support serializer on Class for now"); |
| } |
| builder.addObjectConverter( |
| Class.class.cast(args[0]), (ObjectConverter.Writer) (instance, jsonbGenerator) -> |
| s.serialize( |
| instance, jsonbGenerator.getJsonGenerator(), |
| new JohnzonSerializationContext(jsonbGenerator))); |
| }); |
| }); |
| config.getProperty(JsonbConfig.DESERIALIZERS).map(JsonbDeserializer[].class::cast).ifPresent(deserializers -> { |
| Stream.of(deserializers).forEach(d -> { |
| final ParameterizedType pt = types.findParameterizedType(d.getClass(), JsonbDeserializer.class); |
| final Type[] args = pt.getActualTypeArguments(); |
| if (args.length != 1 || !Class.class.isInstance(args[0])) { |
| throw new IllegalArgumentException("We only support deserializer on Class for now"); |
| } |
| // TODO: support PT in ObjectConverter (list) |
| final JsonBuilderFactory builderFactory = builderFactorySupplier.get(); |
| builder.addObjectConverter( |
| Class.class.cast(args[0]), (ObjectConverter.Reader) |
| (jsonObject, targetType, parser) -> d.deserialize( |
| JsonValueParserAdapter.createFor(jsonObject, parserFactoryProvider), |
| new JohnzonDeserializationContext(parser, builderFactory, jsonp), targetType)); |
| }); |
| }); |
| |
| if (Closeable.class.isInstance(accessMode)) { |
| builder.addCloseable(Closeable.class.cast(accessMode)); |
| } |
| return doCreateJsonb(skipCdi, ijson, builder.build()); |
| } |
| |
| private <T extends Enum<T>> MapperConfig.CustomEnumConverter<T> newEnumConverter(final Class<T> enumType) { |
| return new JsonbEnumAdapter<>(enumType); |
| } |
| |
| // note: this method must stay as small as possible to enable graalvm to replace it by "false" when needed |
| private Jsonb doCreateJsonb(final boolean skipCdi, final boolean ijson, final Mapper mapper) { |
| if (!skipCdi && cdiIntegration != null && cdiIntegration.isCanWrite()) { |
| final JohnzonJsonb jsonb = new JohnzonJsonb(mapper, ijson, i -> { |
| if (cdiIntegration.isCanWrite()) { |
| cdiIntegration.untrack(i); |
| } |
| }); |
| cdiIntegration.track(jsonb); |
| return jsonb; |
| } |
| return new JohnzonJsonb(mapper, ijson, null); |
| } |
| |
| private Boolean toBool(final Object v) { |
| return !Boolean.class.isInstance(v) ? Boolean.parseBoolean(v.toString()) : Boolean.class.cast(v); |
| } |
| |
| private AccessMode toAccessMode(final Object s) { |
| if (String.class.isInstance(s)) { |
| try { |
| return AccessMode.class.cast( |
| Thread.currentThread().getContextClassLoader().loadClass(s.toString()).getConstructor().newInstance()); |
| } catch (final InstantiationException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) { |
| throw new IllegalArgumentException(e); |
| } catch (InvocationTargetException e) { |
| throw new IllegalArgumentException(e.getCause()); |
| } |
| } |
| return AccessMode.class.cast(s); |
| } |
| |
| private Supplier<JsonParserFactory> createJsonParserFactory() { |
| return new Lazy<JsonParserFactory>() { // thread safety is not mandatory |
| @Override |
| protected JsonParserFactory doCreate() { |
| return jsonp.createParserFactory(emptyMap()); |
| } |
| }; |
| } |
| |
| private Supplier<JsonBuilderFactory> createJsonBuilderFactory() { |
| return new Lazy<JsonBuilderFactory>() { // thread safety is not mandatory |
| @Override |
| protected JsonBuilderFactory doCreate() { |
| return jsonp.createBuilderFactory(emptyMap()); |
| } |
| }; |
| } |
| |
| private Object getBeanManager() { |
| if (beanManager == null) { |
| try { // don't trigger CDI if not there |
| final Class<?> cdi = tccl().loadClass("javax.enterprise.inject.spi.CDI"); |
| final Object cdiInstance = cdi.getMethod("current").invoke(null); |
| beanManager = cdi.getMethod("getBeanManager").invoke(cdiInstance); |
| cdiIntegration = new CDIs(beanManager); |
| } catch (final NoClassDefFoundError | Exception e) { |
| beanManager = NO_BM; |
| } |
| } |
| return beanManager; |
| } |
| |
| private JohnzonAdapterFactory findFactory(final boolean skipCdi) { |
| if (skipCdi || getBeanManager() == NO_BM) { |
| return new SimpleJohnzonAdapterFactory(); |
| } |
| try { // don't trigger CDI is not there |
| return new org.apache.johnzon.jsonb.factory.CdiJohnzonAdapterFactory(beanManager); |
| } catch (final NoClassDefFoundError | Exception e) { |
| return new SimpleJohnzonAdapterFactory(); |
| } |
| } |
| |
| private Boolean shouldSkipCdi() { |
| return config.getProperty("johnzon.skip-cdi") |
| .map(s -> "true".equalsIgnoreCase(String.valueOf(s))) |
| .orElseGet(() -> !config.getProperty("johnzon.cdi.activated").map(Boolean.class::cast).orElse(Boolean.TRUE)); |
| } |
| |
| private ClassLoader tccl() { |
| return ofNullable(Thread.currentThread().getContextClassLoader()).orElseGet(ClassLoader::getSystemClassLoader); |
| } |
| |
| private Map<String, ?> generatorConfig() { |
| final Map<String, Object> map = new HashMap<>(); |
| if (config == null) { |
| return map; |
| } |
| config.getProperty(JsonGeneratorFactoryImpl.GENERATOR_BUFFER_LENGTH).ifPresent(b -> map.put(JsonGeneratorFactoryImpl.GENERATOR_BUFFER_LENGTH, b)); |
| config.getProperty(AbstractJsonFactory.BUFFER_STRATEGY).ifPresent(b -> map.put(AbstractJsonFactory.BUFFER_STRATEGY, b)); |
| config.getProperty(JsonbConfig.FORMATTING).ifPresent(b -> map.put(JsonGenerator.PRETTY_PRINTING, b)); |
| return map; |
| } |
| |
| private Map<String, ?> readerConfig() { |
| final Map<String, Object> map = new HashMap<>(); |
| if (config == null) { |
| return map; |
| } |
| config.getProperty(JsonParserFactoryImpl.BUFFER_LENGTH).ifPresent(b -> map.put(JsonParserFactoryImpl.BUFFER_LENGTH, b)); |
| config.getProperty(JsonParserFactoryImpl.MAX_STRING_LENGTH).ifPresent(b -> map.put(JsonParserFactoryImpl.MAX_STRING_LENGTH, b)); |
| config.getProperty(JsonParserFactoryImpl.SUPPORTS_COMMENTS).ifPresent(b -> map.put(JsonParserFactoryImpl.SUPPORTS_COMMENTS, b)); |
| config.getProperty(AbstractJsonFactory.BUFFER_STRATEGY).ifPresent(b -> map.put(AbstractJsonFactory.BUFFER_STRATEGY, b)); |
| return map; |
| } |
| |
| private static abstract class Lazy<T> implements Supplier<T> { |
| private final AtomicReference<T> ref = new AtomicReference<>(); |
| |
| @Override |
| public T get() { |
| T factory = ref.get(); |
| if (factory == null) { |
| factory = doCreate(); |
| if (!ref.compareAndSet(null, factory)) { |
| factory = ref.get(); |
| } |
| } |
| return factory; |
| } |
| |
| protected abstract T doCreate(); |
| } |
| } |