EntityRefDeserializer.java

/*
 * Copyright 2023 Global Crop Diversity Trust
 *
 * 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 org.gringlobal.custom.json;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

import lombok.extern.slf4j.Slf4j;
import org.genesys.blocks.model.EmptyModel;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JacksonException;
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.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;


/**
 * EntityRefDeserializer is used to read only the {@code id} from JSON
 * and then use it to create a new instance of the object of target type with
 * the type {@code constructor(Long id)}.
 * 
 * The {@code id} can be wrapped as an object <code>"accession": { "id": 12312 }</code> or as a number {@code "accession": 123}.
 */
@Slf4j
public class EntityRefDeserializer<T extends EmptyModel> extends JsonDeserializer<T> implements ContextualDeserializer {

	private JavaType wrapperType;
	private JavaType refType;
	private Constructor<?> constructor;

	public void setWrapperType(JavaType wrapperType) {
		this.wrapperType = wrapperType;
	}

	public void setRefType(JavaType refType) {
		this.refType = refType;
	}

	public void setConstructor(Constructor<?> constructor) {
		this.constructor = constructor;
	}

	@Override
	public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
		throws JsonMappingException {
		EntityRefDeserializer<?> deserializer = new EntityRefDeserializer<>();
		JavaType wrapperType = property.getType();
		deserializer.wrapperType = wrapperType;
		deserializer.refType = ctxt.getTypeFactory().constructParametricType(EntityRef.class, wrapperType);
		try {
			deserializer.constructor = (Constructor<?>) wrapperType.getRawClass().getConstructor(Long.class);
		} catch (NoSuchMethodException | SecurityException e) {
			throw InvalidDefinitionException.from(ctxt, "Target type does not have a constructor(Long id)");
		}
		return deserializer;
	}

	/**
	 * Deserialize.
	 *
	 * @param p    the p
	 * @param ctxt the ctxt
	 * @return the empty model
	 * @throws IOException      Signals that an I/O exception has occurred.
	 * @throws JacksonException the jackson exception
	 */
	@Override
	public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
		if (log.isTraceEnabled()) {
			log.trace("Deserializing {} {} as {}", p.currentName(), p.currentToken(), wrapperType);
		}
		EntityRef<T> ref = null;
		if (p.currentToken() == JsonToken.START_OBJECT) {
			ref = ctxt.readValue(p, refType);
		} else if (p.currentToken() == JsonToken.VALUE_NUMBER_INT) {
			ref = new EntityRef<>(p.getLongValue());
		} else {
			throw new JsonParseException(p, "Expecting an object or a number");
		}
		if (ref != null && constructor != null) {
			return ref.get((Constructor<T>) constructor);
		}
		return null;
	}

	public static class EntityRef<R> implements Serializable {
		private static final long serialVersionUID = 4386291137091176433L;
		public Long id;

		public EntityRef() {
		}

		public R get(Constructor<R> constructor) {
			try {
				return constructor.newInstance(id);
			} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
				log.error("Cannot make: {}", e.getMessage(), e);
			}
			return null;
		}

		@JsonCreator
		public EntityRef(@JsonProperty("id") Long id) {
			this.id = id;
		}
	}
}