content: /*
   *  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.causeway.commons.io;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.util.List;
  import java.util.Optional;
  import java.util.function.UnaryOperator;
  
  import jakarta.xml.bind.annotation.adapters.XmlAdapter;
  
  import com.fasterxml.jackson.annotation.JsonInclude.Include;
  import com.fasterxml.jackson.core.JacksonException;
  import com.fasterxml.jackson.core.JsonGenerator;
  import com.fasterxml.jackson.core.JsonParser;
  import com.fasterxml.jackson.databind.BeanProperty;
  import com.fasterxml.jackson.databind.DeserializationContext;
  import com.fasterxml.jackson.databind.DeserializationFeature;
  import com.fasterxml.jackson.databind.JsonDeserializer;
  import com.fasterxml.jackson.databind.JsonMappingException;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import com.fasterxml.jackson.databind.SerializationFeature;
  import com.fasterxml.jackson.databind.SerializerProvider;
  import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
  import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
  import com.fasterxml.jackson.databind.module.SimpleModule;
  import com.fasterxml.jackson.databind.ser.std.StdSerializer;
  import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
  import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
  import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationModule;
  
  import org.jspecify.annotations.Nullable;
  
  import org.apache.causeway.commons.collections.Can;
  import org.apache.causeway.commons.functional.Try;
  import org.apache.causeway.commons.internal.base._Casts;
  import org.apache.causeway.commons.internal.context._Context;
  import org.apache.causeway.commons.internal.exceptions._Exceptions;
  import org.apache.causeway.commons.internal.reflection._Generics;
  
  import org.jspecify.annotations.NonNull;
  import lombok.SneakyThrows;
  import lombok.experimental.UtilityClass;
  import lombok.extern.log4j.Log4j2;
  
  /**
   * Utilities to convert from and to JSON format.
   *
   * @since 2.0 {@index}
   */
  @UtilityClass
  @Log4j2
  public class JsonUtils {
  
      /**
       * Consumers of the framework may choose to use a different provider.
       */
      public Optional<Class<?>> getPlatformDefaultJsonProviderForJaxb() {
          return Try.call(()->_Context.loadClass("org.eclipse.persistence.jaxb.rs.MOXyJsonProvider"))
                  .ifFailure(cause->
                        log.warn("This implementation of RestfulClient does require the class 'MOXyJsonProvider'"
                            + " on the class-path."
                            + " Are you missing a maven dependency?")
                  )
                  .getValue()
                  .map(x->x);
      }
  
      @FunctionalInterface
      public interface JacksonCustomizer extends UnaryOperator<ObjectMapper> {
          public static <T> JacksonCustomizer wrapXmlAdapter(final XmlAdapter<String, T> xmlAdapter) {
              @SuppressWarnings("unchecked")
              var type = (Class<T>) _Generics.streamGenericTypeArgumentsOfType(xmlAdapter.getClass(), XmlAdapter.class)
                  .skip(1)
                  .findFirst()
                  .orElseThrow(()->_Exceptions.unsupportedOperation(
                          "Failed to autodetect second generic type argument of class %s. "
                          + "Use variant JacksonCustomizer.wrapXmlAdapter(type, xmlAdapter) "
                          + "to provide the type explicitely.",
                          xmlAdapter.getClass().getName()));
              return wrapXmlAdapter(type, xmlAdapter);
          }
          public static <T> JacksonCustomizer wrapXmlAdapter(final Class<T> type, final XmlAdapter<String, T> xmlAdapter) {
              return mapper->
                  mapper.registerModule(new SimpleModule()
                          .addSerializer(new XSerializer<T>(type, xmlAdapter))
                          .addDeserializer(type, new XDeserializer<T>(type, xmlAdapter)));
          }
      }
  
      // -- READING
  
      /**
       * Tries to deserialize JSON content from given UTF8 encoded {@link String}
       * into an instance of given {@code mappedType}.
       */
      public <T> Try<T> tryRead(
              final @NonNull Class<T> mappedType,
              final @Nullable String stringUtf8,
              final JsonUtils.JacksonCustomizer ... customizers) {
          return tryRead(mappedType, DataSource.ofStringUtf8(stringUtf8), customizers);
      }
  
      /**
       * Tries to deserialize JSON content from given {@link DataSource} into an instance of
       * given {@code mappedType}.
       */
      public <T> Try<T> tryRead(
              final @NonNull Class<T> mappedType,
              final @NonNull DataSource source,
              final JsonUtils.JacksonCustomizer ... customizers) {
          return source.tryReadAll((final InputStream is)->{
              return Try.call(()->createJacksonReader(customizers).readValue(is, mappedType));
          });
      }
  
      /**
       * Tries to deserialize JSON content from given {@link DataSource} into a {@link List}
       * with given {@code elementType}.
       */
      public <T> Try<List<T>> tryReadAsList(
              final @NonNull Class<T> elementType,
              final @NonNull DataSource source,
              final JsonUtils.JacksonCustomizer ... customizers) {
          return source.tryReadAll((final InputStream is)->{
              return Try.call(()->{
                  var mapper = createJacksonReader(customizers);
                  var collectionType = mapper.getTypeFactory().constructCollectionType(List.class, elementType);
                  return mapper.readValue(is, collectionType);
              });
          });
      }
  
      // -- WRITING
  
      /**
       * Writes given {@code pojo} to given {@link DataSink}.
       */
      public void write(
              final @Nullable Object pojo,
              final @NonNull DataSink sink,
              final JsonUtils.JacksonCustomizer ... customizers) {
          if(pojo==null) return;
          sink.writeAll(os->
              Try.run(()->createJacksonWriter(customizers).writeValue(os, pojo)));
      }
  
      /**
       * Converts given {@code pojo} to an UTF8 encoded {@link String}.
       * @return <code>null</code> if pojo is <code>null</code>
       */
      @SneakyThrows
      @Nullable
      public static String toStringUtf8(
              final @Nullable Object pojo,
              final JsonUtils.JacksonCustomizer ... customizers) {
          return pojo!=null
                  ? createJacksonWriter(customizers).writeValueAsString(pojo)
                  : null;
      }
  
      // -- CUSTOMIZERS
  
      /** enable indentation for the underlying generator */
      public ObjectMapper indentedOutput(final ObjectMapper mapper) {
          return mapper.enable(SerializationFeature.INDENT_OUTPUT);
      }
  
      /** only properties with non-null values are to be included */
      public ObjectMapper onlyIncludeNonNull(final ObjectMapper mapper) {
          return mapper.setSerializationInclusion(Include.NON_NULL);
      }
  
      /** add support for JDK 8, e.g. {@link Optional} */
      public ObjectMapper jdk8Support(final ObjectMapper mapper) {
          return mapper.registerModule(new Jdk8Module());
      }
  
      /** add support for JAXB annotations */
      public ObjectMapper jaxbAnnotationSupport(final ObjectMapper mapper) {
          return mapper.registerModule(new JakartaXmlBindAnnotationModule());
      }
  
      /** add support for reading java.time (ISO) */
      public ObjectMapper readingJavaTimeSupport(final ObjectMapper mapper) {
          mapper.registerModule(new JavaTimeModule());
          mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
          return mapper;
      }
  
      /** add support for writing java.time (ISO) */
      public ObjectMapper writingJavaTimeSupport(final ObjectMapper mapper) {
          mapper.registerModule(new JavaTimeModule());
          mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
          return mapper;
      }
  
      // -- CAN SUPPORT
  
      static class CanDeserializer extends JsonDeserializer<Can<?>> implements ContextualDeserializer {
          private Class<?> elementType;
          public CanDeserializer(final @NonNull Class<?> elementType) {
              this.elementType = elementType;
          }
          @Override
          public JsonDeserializer<?> createContextual(final DeserializationContext ctxt, final BeanProperty beanProperty) throws JsonMappingException {
              var type = ctxt.getContextualType() != null
                  ? ctxt.getContextualType()
                  : beanProperty!=null
                      ? beanProperty.getMember().getType()
                      : null;
              var elementType = type!=null && type.containedTypeCount()==1
                      ? type.containedType(0).getRawClass()
                      : Object.class;
              return new CanDeserializer(elementType);
          }
          @Override
          public Can<?> deserialize(
                  final JsonParser p, final DeserializationContext ctxt) throws IOException {
              var listType = ctxt.getTypeFactory().constructCollectionType(List.class, elementType);
              var list = ctxt.readValue(p, listType);
              return Can.ofCollection(_Casts.uncheckedCast(list));
          }
      }
      /** add support for reading Can<T> */
      public ObjectMapper readingCanSupport(final ObjectMapper mapper) {
          mapper.registerModule(new SimpleModule().addDeserializer(Can.class, new CanDeserializer(Object.class)));
          return mapper;
      }
  
      static class CanSerializer extends StdSerializer<Can<?>> {
          private static final long serialVersionUID = 1L;
          protected CanSerializer() { super(Can.class, false); }
          @Override
          public void serialize(final Can<?> value, final JsonGenerator gen,
                  final SerializerProvider provider) throws IOException {
              gen.writeObject(value.toList());
          }
      }
      /** add support for writing Can<T> */
      public ObjectMapper writingCanSupport(final ObjectMapper mapper) {
          mapper.registerModule(new SimpleModule().addSerializer(new CanSerializer()));
          return mapper;
      }
  
      // -- XML ADAPTER SUPPORT
  
      static class XSerializer<T> extends StdSerializer<T> {
          private static final long serialVersionUID = 1L;
          private final XmlAdapter<String, T> xmlAdapter;
          protected XSerializer(final Class<T> type, final XmlAdapter<String, T> xmlAdapter) {
              super(type, false);
              this.xmlAdapter = xmlAdapter;
          }
          @Override
          public void serialize(final T value, final JsonGenerator gen,
                  final SerializerProvider provider) throws IOException {
              String stringified;
              try {
                  stringified = this.xmlAdapter.marshal(value);
              } catch (Exception e) {
                  throw new JsonMappingException(gen, "Unable to marshal: " + e.getMessage(), e);
              }
              gen.writeObject(stringified);
          }
      }
  
      static class XDeserializer<T> extends StdDeserializer<T> {
          private static final long serialVersionUID = 1L;
          private final XmlAdapter<String, T> xmlAdapter;
          protected XDeserializer(final Class<T> type, final XmlAdapter<String, T> xmlAdapter) {
              super(type);
              this.xmlAdapter = xmlAdapter;
          }
          @Override
          public T deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException, JacksonException {
              String stringified = ctxt.readValue(p, String.class);
              try {
                  return xmlAdapter.unmarshal(stringified);
              } catch (Exception e) {
                  throw new JsonMappingException(p, "Unable to unmarshal (to type " + _valueType + "): " + e.getMessage(), e);
              }
          }
      }
  
      // -- MAPPER FACTORY
  
      private ObjectMapper createJacksonReader(
              final JsonUtils.JacksonCustomizer ... customizers) {
          var mapper = new ObjectMapper();
          mapper = jdk8Support(mapper);
          mapper = readingJavaTimeSupport(mapper);
          mapper = readingCanSupport(mapper);
          for(JsonUtils.JacksonCustomizer customizer : customizers) {
              mapper = Optional.ofNullable(customizer.apply(mapper))
                      .orElse(mapper);
          }
          return mapper;
      }
  
      private ObjectMapper createJacksonWriter(
              final JsonUtils.JacksonCustomizer ... customizers) {
          var mapper = new ObjectMapper();
          mapper = jdk8Support(mapper);
          mapper = writingJavaTimeSupport(mapper);
          mapper = writingCanSupport(mapper);
          for(JsonUtils.JacksonCustomizer customizer : customizers) {
              mapper = Optional.ofNullable(customizer.apply(mapper))
                      .orElse(mapper);
          }
          return mapper;
      }
  
  }
  