All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.konduto.sdk.models.KondutoModel Maven / Gradle / Ivy

Go to download

Easily integrate with Konduto (https://konduto.com), a fraud prevention service.

There is a newer version: 2.17.4
Show newest version
package com.konduto.sdk.models;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import com.konduto.sdk.adapters.KondutoPaymentAdapter;
import com.konduto.sdk.adapters.KondutoShoppingCartAdapter;
import com.konduto.sdk.adapters.KondutoTravelAdapter;
import com.konduto.sdk.annotations.Required;
import com.konduto.sdk.annotations.ValidateFormat;
import com.konduto.sdk.exceptions.KondutoInvalidEntityException;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.*;

/**
 *
 * This is the parent of all models.
 *
 */
public abstract class KondutoModel {
	protected KondutoModel(){ }

	@Override
	public abstract boolean equals(Object obj);

	/* Transient and static attributes won't be included in serialization */
	private static Type paymentsType = new TypeToken>(){}.getType();
	private static Type shoppingCartType = new TypeToken>(){}.getType();
    private static Type travelType = new TypeToken(){}.getType();

	protected static Gson gson = new GsonBuilder()
			.registerTypeAdapter(paymentsType, new KondutoPaymentAdapter())
			.registerTypeAdapter(shoppingCartType, new KondutoShoppingCartAdapter())
			.registerTypeAdapter(travelType, new KondutoTravelAdapter())
			.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
            .setDateFormat("yyyy-MM-dd")
			.create();

	protected transient List errors = new ArrayList<>();

	/* Serialization methods */

	/**
	 * Serializes a model instance to JSON.
	 * @return a {@link com.google.gson.JsonObject}
	 * @throws KondutoInvalidEntityException
	 */
	public JsonObject toJSON() throws KondutoInvalidEntityException{
		if(!this.isValid()) { throw new KondutoInvalidEntityException(this); }
		return (JsonObject) gson.toJsonTree(this);
	}

	/**
	 * Converts a {@link com.google.gson.JsonObject} to a model instance.
	 * @param json the serialized instance
	 * @param klass the instance class
	 * @return an instance of KondutoModel (e.g a KondutoAddress if klass is {@code KondutoAddress.class})
	 */
	public static KondutoModel fromJSON(JsonObject json, Class klass){
		return (KondutoModel) gson.fromJson(json, klass);
	}

	/* Error printing methods */

	/**
	 * @return {@link com.konduto.sdk.models.KondutoModel#errors errors} pretty printed.
	 */
	public String getErrors(){
		StringBuilder errors = new StringBuilder();
		for(String error : this.errors) {
			errors.append("\n");
			errors.append(error);
		}
		return this.getClass().getSimpleName() + errors.toString();
	}

	/**
	 * Adds a 'is required' message to {@link com.konduto.sdk.models.KondutoModel#errors errors}
	 *
	 * @param field the incorrect field
	 * @param value the incorrect field value
	 */
	void addIsRequiredError(Field field, Object value) {
		if(value != null) {
			this.errors.add("" +
					"\t" +
					field.getName() + " of class " +
					value.getClass().getSimpleName() +
					" is required but came " +
					'\'' + value + '\'');
		} else {
			this.errors.add("\t" + field.getName() + " is required but came null");
		}

	}

	/**
	 * Validates whether a string field's value matches a given regex.
	 * If it doesn't then add an error to the errors collection.
	 * @param field the field
	 * @param value the value
	 * @param format the format (a Java regex)
	 */
    private void addInvalidFormatError(Field field, Object value, String format) {
        this.errors.add("" +
                "\t" +
                field.getName() + " value is " + value + " which format does not match " + '\'' + format + '\'');
    }

	/**
	 *
	 * @param errors a String containing a
	 * {@link com.konduto.sdk.models.KondutoModel#errors KondutoModel instance errors}
	 */
	void addIsInvalidError(String errors) {
		this.errors.add(errors);
	}

	/**
	 * @return whether this KondutoModel instance is valid or not.
	 */
	/* Validation method */
	public boolean isValid() {
		errors.clear();
		Object value;
		for(Field f : this.getClass().getDeclaredFields()) {
			if (!f.isSynthetic()) {
				try {
					f.setAccessible(true);
					value = f.get(this);

					// validates requirement
					if(f.isAnnotationPresent(Required.class)){
						if(value == null) {
							addIsRequiredError(f, null);
						} else {
							Method isEmptyMethod = value.getClass().getMethod("isEmpty");
							boolean isEmpty = (boolean) isEmptyMethod.invoke(value);
							if(isEmpty) {
								addIsRequiredError(f, value);
							}
						}
					}

                    if(f.isAnnotationPresent(ValidateFormat.class)){
                        String format = f.getAnnotation(ValidateFormat.class).format();
                        if (value != null) {
                            boolean match = ((String) value).matches(format);
                            if(!match) {
                                addInvalidFormatError(f, value, format);
                            }
                        }
                    }

					// if the field is a KondutoModel, check if it is valid
					if (value instanceof KondutoModel) {
						if(!((KondutoModel) value).isValid()) {
							addIsInvalidError(((KondutoModel) value).getErrors());
						}
					}
				} catch (NoSuchMethodException e) {
					// no problem if method does not exist;
				} catch (IllegalAccessException e) {
					throw new RuntimeException("Illegal access to a required field should never happen.");
				} catch (InvocationTargetException e) {
					throw new RuntimeException();
				}
			}
		}

		return errors.isEmpty();

	}


    /**
	 * Enables Map-based construction in KondutoModel children.
	 *
	 * @param attributes a {@link HashMap} containing attributes. For a field 'totalAmount' with type Long, we should
	 *                   add the following entry to the map: 'totalAmount', 123L.
	 */
	public static KondutoModel fromMap(Map attributes, Class klass){

		KondutoModel model;

		try {
			model = (KondutoModel) klass.newInstance();
		} catch (InstantiationException e) {
			e.printStackTrace();
			throw new RuntimeException("could not instantiate an object of " + klass);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("constructor is not accessible in " + klass);
		}



		for(Map.Entry attribute : attributes.entrySet()) {

			String attributeName = attribute.getKey();

			try {
				Field field = klass.getDeclaredField(attributeName);

				Object value = attribute.getValue();

				if(!relatedClasses(field.getType(), value.getClass())){
					throw new IllegalArgumentException(String.format(
							"Illegal value for attribute %s. Expected a value of class %s, but got a value of class %s",
							field.getName(),
							field.getType(),
							value.getClass()
					));
				}

				field.setAccessible(true);

				field.set(model, value);

			} catch (NoSuchFieldException e) {
				throw new IllegalArgumentException(String.format("Attribute %s was not found.", attributeName));
			} catch (IllegalAccessException e) {
				throw new RuntimeException("if field was found it should be accessible (via field.setAccessible(true))");
			}
		}

		return model;

	}

	/**
	 * Classes are related iff class1 is the same as class2 or if one of them is a wrapper for the other one
	 * (e.g class1 is int.class and class2 is Integer.class)
	 * @param class1 a class
	 * @param class2 another class
	 * @return whether class1 and class2 are related
	 */
	private static boolean relatedClasses(Class class1, Class class2) {
		if(class1.equals(class2)) return true;
		if(isWrapped(class1, class2)) return true;
		if(isWrapped(class2, class1)) return true;
		return false;
	}

	/**
	 * Checks whether class1 is wrapped by class2.
	 * @param class1 supposedly wrapped class.
	 * @param class2 supposedly wrapper class.
	 * @return true if class1 is wrapped by class2 or false otherwise.
	 */
	private static boolean isWrapped(Class class1, Class class2) {
		if(class1.equals(boolean.class) && class2.equals(Boolean.class)) return true;
		if(class1.equals(byte.class) && class2.equals(Byte.class)) return true;
		if(class1.equals(short.class) && class2.equals(Short.class)) return true;
		if(class1.equals(char.class) && class2.equals(Character.class)) return true;
		if(class1.equals(int.class) && class2.equals(Integer.class)) return true;
		if(class1.equals(long.class) && class2.equals(Long.class)) return true;
		if(class1.equals(float.class) && class2.equals(Float.class)) return true;
		if(class1.equals(double.class) && class2.equals(Double.class)) return true;
		return false;
	}

	/**
	 * Fluent constructor implementation
	 * @param attributeName the attribute name (e.g greeting)
	 * @param attributeValue the attribute value (e.g "Hello")
	 * @return a new instance
	 *
	 * @see Fluent interface article
	 */
	public KondutoModel with(String attributeName, Object attributeValue){
		try {
			Field field = this.getClass().getDeclaredField(attributeName);
			field.setAccessible(true);
			field.set(this, attributeValue);
		} catch (NoSuchFieldException e) {
			throw new RuntimeException("field " + attributeName + " was not found.");
		} catch (IllegalAccessException e) {
			throw new RuntimeException("field " + attributeName + "was found. Therefore it should be accessible.");
		}
		return this;
	}

	protected boolean nullSafeAreDatesEqual(Date one, Date two){
		if ((one == null && two == null) ||
				((one != null && two != null) && one.compareTo(two) == 0))
			return true;

		return false;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy