I18nStringJacksonDeserializer.java

package dev.orne.i18n;

/*-
 * #%L
 * Orne I18N
 * %%
 * Copyright (C) 2021 Orne Developments
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #L%
 */

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.validation.constraints.NotNull;

import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;

/**
 * Jackson JSON deserializer for {@code I18nString} instances.
 * 
 * @author <a href="https://github.com/ihernaez">(w) Iker Hernaez</a>
 * @version 1.0, 2021-02
 * @see I18nString
 * @since 0.1
 */
@API(status=Status.INTERNAL, since="0.1")
public class I18nStringJacksonDeserializer
extends JsonDeserializer<I18nString>
implements ContextualDeserializer {

    /** The by target type cache. */
    private final Map<Class<?>, I18nStringJacksonDeserializer> cache =
            new HashMap<>();
    /** The converter from {@code I18nString} to the target type. */
    private final I18nStringConverter converter;

    /**
     * Creates a new instance.
     */
    public I18nStringJacksonDeserializer() {
        super();
        this.converter = null;
    }

    /**
     * Creates a new instance.
     * 
     * @param converter The converter from {@code I18nString} to the target type
     */
    protected I18nStringJacksonDeserializer(
            final I18nStringConverter converter) {
        super();
        this.converter = converter;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JsonDeserializer<?> createContextual(
            final @NotNull DeserializationContext ctxt,
            final BeanProperty property)
    throws JsonMappingException {
        final Class<?> targetType;
        if (property == null) {
            targetType = ctxt.getContextualType().getRawClass();
        } else {
            targetType = property.getType().getRawClass();
        }
        if (!I18nString.class.equals(targetType) ) {
            return cache.computeIfAbsent(targetType, type -> {
                final I18nStringConverter typeConverter = getConverter(targetType);
                if (typeConverter == null) {
                    return this;
                } else {
                    return new I18nStringJacksonDeserializer(typeConverter);
                }
            });
        }
        return this;
    }

    /**
     * Returns a converter that converts {@code I18nString} instances to the
     * specified target type.
     * 
     * @param targetType The target type.
     * @return The {@code I18nString} type converter.
     */
    protected I18nStringConverter getConverter(
            final @NotNull Class<?> targetType) {
        final I18nStringConverter result;
        if (I18nFixedString.class.equals(targetType)) {
            result = I18nFixedString::from;
        } else if (I18nStringMap.class.equals(targetType)) {
            result = I18nStringMap::new;
        } else {
            result = null;
        }
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public I18nString deserialize(
            final @NotNull JsonParser parser,
            final @NotNull DeserializationContext context)
    throws IOException {
        I18nString result = null;
        if (parser.hasCurrentToken()) {
            JsonToken token = parser.getCurrentToken();
            if (token == JsonToken.START_OBJECT) {
                result = new I18nStringMap(parser.readValueAs(MapFromObject.class));
            } else if (token == JsonToken.VALUE_STRING) {
                result = I18nFixedString.from(parser.getText());
            } else if (token != JsonToken.VALUE_NULL) {
                throw new JsonParseException(parser, "Unexpected token.", parser.getTokenLocation());
            }
        }
        if (result != null && this.converter != null) {
            result = this.converter.convert(result);
        }
        return result;
    }

    /**
     * Extension of {@code I18nStringMap} for deserialization from
     * JSON object.
     * 
     * @author <a href="https://github.com/ihernaez">(w) Iker Hernaez</a>
     * @version 1.0, 2021-02
     * @see I18nStringMap
     * @since 0.1
     */
    @API(status=Status.INTERNAL, since="0.1")
    @JsonDeserialize(using = JsonDeserializer.None.class)
    private static  class MapFromObject
    extends I18nStringMap {

        /** The serial version UID. */
        private static final long serialVersionUID = 1L;
    }
}