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

is.codion.framework.domain.test.DefaultEntityFactory Maven / Gradle / Ivy

/*
 * This file is part of Codion.
 *
 * Codion is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Codion 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 Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Codion.  If not, see .
 *
 * Copyright (c) 2021 - 2024, Björn Darri Sigurðsson.
 */
package is.codion.framework.domain.test;

import is.codion.common.db.exception.DatabaseException;
import is.codion.common.item.Item;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.domain.entity.Entities;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityDefinition;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.attribute.Attribute;
import is.codion.framework.domain.entity.attribute.AttributeDefinition;
import is.codion.framework.domain.entity.attribute.Column;
import is.codion.framework.domain.entity.attribute.ColumnDefinition;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import is.codion.framework.domain.entity.attribute.ForeignKeyDefinition;
import is.codion.framework.domain.test.DomainTest.EntityFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.stream.IntStream;

import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

/**
 * Handles creating and manipulating Entity instances for testing purposes.
 */
public class DefaultEntityFactory implements EntityFactory {

	private static final Logger LOG = LoggerFactory.getLogger(DefaultEntityFactory.class);

	private static final int MININUM_RANDOM_NUMBER = -10_000;
	private static final int MAXIMUM_RANDOM_NUMBER = 10_000;
	private static final int MAXIMUM_RANDOM_STRING_LENGTH = 10;
	private static final String ALPHA_NUMERIC = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
	private static final Random RANDOM = new Random();

	private final EntityConnection connection;
	private final Entities entities;
	private final Map foreignKeyEntities = new HashMap<>();

	/**
	 * Instantiates a new {@link DefaultEntityFactory}
	 * @param connection the connection to use
	 */
	public DefaultEntityFactory(EntityConnection connection) {
		this.connection = requireNonNull(connection);
		this.entities = connection.entities();
	}

	@Override
	public Entity entity(EntityType entityType) throws DatabaseException {
		Entity entity = entities.entity(requireNonNull(entityType));
		populate(entity, insertableColumns(entities.definition(entityType)));

		return entity;
	}

	@Override
	public Optional entity(ForeignKey foreignKey) throws DatabaseException {
		if (entities.definition(requireNonNull(foreignKey).referencedType()).readOnly()) {
			return Optional.empty();
		}
		if (foreignKeyEntities.containsKey(foreignKey)) {
			return Optional.ofNullable(foreignKeyEntities.get(foreignKey));
		}

		foreignKeyEntities.put(foreignKey, null);// short curcuit recursion
		Entity entity = insertOrSelect(entity(foreignKey.referencedType()), connection);
		foreignKeyEntities.put(foreignKey, entity);

		return Optional.of(entity);
	}

	@Override
	public void modify(Entity entity) throws DatabaseException {
		populate(requireNonNull(entity), updatableColumns(entity.definition()));
	}

	/**
	 * @return the underlying {@link EntityConnection} instance
	 */
	protected final EntityConnection connection() {
		return connection;
	}

	/**
	 * @return the underlying {@link Entities} instance
	 */
	protected final Entities entities() {
		return entities;
	}

	/**
	 * Creates a value for the given attribute.
	 * @param attribute the attribute
	 * @param  the attribute value type
	 * @return a random value
	 * @throws DatabaseException in case of an exception
	 */
	protected  T value(Attribute attribute) throws DatabaseException {
		requireNonNull(attribute, "attribute");
		AttributeDefinition attributeDefinition = entities.definition(attribute.entityType()).attributes().definition(attribute);
		try {
			if (attributeDefinition instanceof ForeignKeyDefinition) {
				return (T) entity(((ForeignKeyDefinition) attributeDefinition).attribute()).orElse(null);
			}
			if (!attributeDefinition.items().isEmpty()) {
				return randomItem(attributeDefinition);
			}
			if (attribute.type().isBoolean()) {
				return (T) Boolean.valueOf(RANDOM.nextBoolean());
			}
			if (attribute.type().isCharacter()) {
				return (T) Character.valueOf((char) RANDOM.nextInt());
			}
			if (attribute.type().isLocalDate()) {
				return (T) LocalDate.now();
			}
			if (attribute.type().isLocalDateTime()) {
				return (T) LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
			}
			if (attribute.type().isOffsetDateTime()) {
				return (T) OffsetDateTime.now().truncatedTo(ChronoUnit.SECONDS);
			}
			if (attribute.type().isLocalTime()) {
				return (T) LocalTime.now().truncatedTo(ChronoUnit.SECONDS);
			}
			if (attribute.type().isDouble()) {
				return (T) Double.valueOf(randomDouble(attributeDefinition));
			}
			if (attribute.type().isBigDecimal()) {
				return (T) BigDecimal.valueOf(randomDouble(attributeDefinition));
			}
			if (attribute.type().isInteger()) {
				return (T) Integer.valueOf(randomInteger(attributeDefinition));
			}
			if (attribute.type().isLong()) {
				return (T) Long.valueOf(randomLong(attributeDefinition));
			}
			if (attribute.type().isShort()) {
				return (T) Short.valueOf(randomShort(attributeDefinition));
			}
			if (attribute.type().isString()) {
				return (T) randomString(attributeDefinition);
			}
			if (attribute.type().isByteArray()) {
				return (T) randomByteArray(attributeDefinition);
			}
			if (attribute.type().isEnum()) {
				return randomEnum(attribute);
			}

			return null;
		}
		catch (Exception e) {
			LOG.error("Exception while fetching a value for: {}", attributeDefinition.attribute(), e);
			if (e instanceof RuntimeException) {
				throw e;
			}
			if (e instanceof DatabaseException) {
				throw e;
			}

			throw new RuntimeException(e);
		}
	}

	private void populate(Entity entity, Collection> columns) throws DatabaseException {
		EntityDefinition definition = entity.definition();
		for (Column column : columns) {
			if (!definition.foreignKeys().foreignKeyColumn(column)) {
				entity.put((Attribute) column, value(column));
			}
		}
		for (ForeignKey foreignKey : entity.definition().foreignKeys().get()) {
			Entity value = value(foreignKey);
			if (value != null) {
				entity.put(foreignKey, value);
			}
		}
	}

	private static List> insertableColumns(EntityDefinition entityDefinition) {
		return entityDefinition.columns().definitions().stream()
						.filter(column -> column.insertable() && (!entityDefinition.primaryKey().generated() || !column.primaryKey()))
						.map(ColumnDefinition::attribute)
						.collect(toList());
	}

	private static List> updatableColumns(EntityDefinition entityDefinition) {
		return entityDefinition.columns().definitions().stream()
						.filter(column -> column.updatable() && !column.primaryKey())
						.map(ColumnDefinition::attribute)
						.collect(toList());
	}

	private static String randomString(AttributeDefinition attributeDefinition) {
		int length = attributeDefinition.maximumLength() < 0 ? MAXIMUM_RANDOM_STRING_LENGTH : attributeDefinition.maximumLength();

		return IntStream.range(0, length)
						.mapToObj(i -> String.valueOf(ALPHA_NUMERIC.charAt(RANDOM.nextInt(ALPHA_NUMERIC.length()))))
						.collect(joining());
	}

	private static byte[] randomByteArray(AttributeDefinition attributeDefinition) {
		if (attributeDefinition.attribute().type().isByteArray() &&
						attributeDefinition instanceof ColumnDefinition &&
						!((ColumnDefinition) attributeDefinition).lazy()) {
			return randomByteArray(1024);
		}

		return null;
	}

	private static byte[] randomByteArray(int numberOfBytes) {
		byte[] bytes = new byte[numberOfBytes];
		RANDOM.nextBytes(bytes);

		return bytes;
	}

	private static  T randomEnum(Attribute attribute) {
		Object[] enumConstants = attribute.type().valueClass().getEnumConstants();

		return (T) enumConstants[RANDOM.nextInt(enumConstants.length)];
	}

	private static  T randomItem(AttributeDefinition attributeDefinition) {
		List> items = attributeDefinition.items();
		Item item = items.get(RANDOM.nextInt(items.size()));

		return item.value();
	}

	private static int randomInteger(AttributeDefinition attributeDefinition) {
		int min = attributeDefinition.minimumValue() == null ?
						MININUM_RANDOM_NUMBER : Math.max(attributeDefinition.minimumValue().intValue(), MININUM_RANDOM_NUMBER);
		int max = attributeDefinition.maximumValue() == null ?
						MAXIMUM_RANDOM_NUMBER : Math.min(attributeDefinition.maximumValue().intValue(), MAXIMUM_RANDOM_NUMBER);

		return RANDOM.nextInt((max - min) + 1) + min;
	}

	private static long randomLong(AttributeDefinition attributeDefinition) {
		long min = attributeDefinition.minimumValue() == null ?
						MININUM_RANDOM_NUMBER : Math.max(attributeDefinition.minimumValue().longValue(), MININUM_RANDOM_NUMBER);
		long max = attributeDefinition.maximumValue() == null ?
						MAXIMUM_RANDOM_NUMBER : Math.min(attributeDefinition.maximumValue().longValue(), MAXIMUM_RANDOM_NUMBER);

		return RANDOM.nextLong() % (max - min) + min;
	}

	private static short randomShort(AttributeDefinition attributeDefinition) {
		short min = attributeDefinition.minimumValue() == null ?
						MININUM_RANDOM_NUMBER : (short) Math.max(attributeDefinition.minimumValue().intValue(), MININUM_RANDOM_NUMBER);
		short max = attributeDefinition.maximumValue() == null ?
						MAXIMUM_RANDOM_NUMBER : (short) Math.min(attributeDefinition.maximumValue().intValue(), MAXIMUM_RANDOM_NUMBER);

		return (short) (RANDOM.nextInt((max - min) + 1) + min);
	}

	private static double randomDouble(AttributeDefinition attributeDefinition) {
		double min = attributeDefinition.minimumValue() == null ?
						MININUM_RANDOM_NUMBER : Math.max(attributeDefinition.minimumValue().doubleValue(), MININUM_RANDOM_NUMBER);
		double max = attributeDefinition.maximumValue() == null ?
						MAXIMUM_RANDOM_NUMBER : Math.min(attributeDefinition.maximumValue().doubleValue(), MAXIMUM_RANDOM_NUMBER);

		return RANDOM.nextDouble() * (max - min) + min;
	}

	private static Entity insertOrSelect(Entity entity, EntityConnection connection) throws DatabaseException {
		if (entity.primaryKey().isNotNull()) {
			Collection selected = connection.select(singletonList(entity.primaryKey()));
			if (!selected.isEmpty()) {
				return selected.iterator().next();
			}
		}

		return connection.insertSelect(entity);
	}
}