UpdateOperation.java

package io.github.simplejdbcmapper.core;

import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.beans.BeanWrapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.util.Assert;

import io.github.simplejdbcmapper.exception.MapperException;
import io.github.simplejdbcmapper.exception.OptimisticLockingException;

class UpdateOperation {
	private static final int CACHEABLE_UPDATE_SPECIFIC_PROPERTIES_COUNT = 5;

	private static final String INCREMENTED_VERSION = "[incrementedVersion]";

	private final SimpleJdbcMapperSupport sjmSupport;

	// update sql cache
	// Map key - class name
	// value - the update sql and params
	private final SimpleCache<String, SqlAndParams> updateSqlCache = new SimpleCache<>();

	// update specified properties sql cache
	// Map key - class name and properties
	// value - the update sql and params
	private final SimpleCache<String, SqlAndParams> updateSpecificPropertiesSqlCache = new SimpleCache<>(2000);

	public UpdateOperation(SimpleJdbcMapperSupport sjmSupport) {
		this.sjmSupport = sjmSupport;
	}

	public Integer update(Object object) {
		Assert.notNull(object, "object must not be null");
		TableMapping tableMapping = sjmSupport.getTableMapping(object.getClass());
		SqlAndParams sqlAndParams = updateSqlCache.get(object.getClass().getName());
		if (sqlAndParams == null) {
			sqlAndParams = buildSqlAndParamsForUpdate(tableMapping);
			updateSqlCache.put(object.getClass().getName(), sqlAndParams);
		}
		return updateInternal(object, sqlAndParams, tableMapping);
	}

	public Integer updateSpecificProperties(Object object, String... propertyNames) {
		Assert.notNull(object, "object must not be null");
		Assert.notNull(propertyNames, "propertyNames must not be null");
		TableMapping tableMapping = sjmSupport.getTableMapping(object.getClass());
		SqlAndParams sqlAndParams = null;
		String cacheKey = getUpdateSpecificPropertiesCacheKey(object, propertyNames);
		if (cacheKey != null) {
			sqlAndParams = updateSpecificPropertiesSqlCache.get(cacheKey);
		}
		if (sqlAndParams == null) {
			sqlAndParams = buildSqlAndParamsForUpdateSpecificProperties(tableMapping, propertyNames);
			if (cacheKey != null) {
				updateSpecificPropertiesSqlCache.put(cacheKey, sqlAndParams);
			}
		}
		return updateInternal(object, sqlAndParams, tableMapping);
	}

	SimpleCache<String, SqlAndParams> getUpdateSqlCache() {
		return updateSqlCache;
	}

	SimpleCache<String, SqlAndParams> getUpdateSpecificPropertiesSqlCache() {
		return updateSpecificPropertiesSqlCache;
	}

	private Integer updateInternal(Object object, SqlAndParams sqlAndParams, TableMapping tableMapping) {
		Assert.notNull(object, "object must not be null");
		Assert.notNull(sqlAndParams, "sqlAndParams must not be null");
		BeanWrapper bw = sjmSupport.getBeanWrapper(object);
		if (bw.getPropertyValue(tableMapping.getIdPropertyName()) == null) {
			throw new IllegalArgumentException("Property " + tableMapping.getMappedObjClassName() + "."
					+ tableMapping.getIdPropertyName() + " is the id and must not be null.");
		}
		Set<String> parameters = sqlAndParams.getParams();
		populateAuditProperties(tableMapping, bw, parameters);
		MapSqlParameterSource mapSqlParameterSource = createMapSqlParameterSource(tableMapping, bw, parameters);
		int cnt = -1;
		// if object has property version the version gets incremented on update.
		// throws OptimisticLockingException when update fails.
		if (sqlAndParams.getParams().contains(INCREMENTED_VERSION)) {
			cnt = sjmSupport.getNamedParameterJdbcTemplate().update(sqlAndParams.getSql(), mapSqlParameterSource);
			if (cnt == 0) {
				throw new OptimisticLockingException(object.getClass().getSimpleName()
						+ " update failed due to stale data. Failed for " + tableMapping.getIdColumnName() + " = "
						+ bw.getPropertyValue(tableMapping.getIdPropertyName()) + " and "
						+ tableMapping.getVersionPropertyMapping().getColumnName() + " = "
						+ bw.getPropertyValue(tableMapping.getVersionPropertyMapping().getPropertyName()));
			}
			// update the version in object with new version
			bw.setPropertyValue(tableMapping.getVersionPropertyMapping().getPropertyName(),
					mapSqlParameterSource.getValue(INCREMENTED_VERSION));
		} else {
			cnt = sjmSupport.getNamedParameterJdbcTemplate().update(sqlAndParams.getSql(), mapSqlParameterSource);
		}
		return cnt;
	}

	private void populateAuditProperties(TableMapping tableMapping, BeanWrapper bw, Set<String> parameters) {
		if (tableMapping.hasAutoAssignProperties()) {
			PropertyMapping updatedByPropMapping = tableMapping.getUpdatedByPropertyMapping();
			if (updatedByPropMapping != null && sjmSupport.getRecordAuditedBySupplier() != null
					&& parameters.contains(updatedByPropMapping.getPropertyName())) {
				bw.setPropertyValue(updatedByPropMapping.getPropertyName(),
						sjmSupport.getRecordAuditedBySupplier().get());
			}
			PropertyMapping updatedOnPropMapping = tableMapping.getUpdatedOnPropertyMapping();
			if (updatedOnPropMapping != null && sjmSupport.getRecordAuditedOnSupplier() != null
					&& parameters.contains(updatedOnPropMapping.getPropertyName())) {
				bw.setPropertyValue(updatedOnPropMapping.getPropertyName(),
						sjmSupport.getRecordAuditedOnSupplier().get());
			}
		}
	}

	private MapSqlParameterSource createMapSqlParameterSource(TableMapping tableMapping, BeanWrapper bw,
			Set<String> parameters) {
		MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource();
		for (String paramName : parameters) {
			if (paramName.equals(INCREMENTED_VERSION)) {
				Integer incrementedVersionVal = getIncrementedVersionValue(tableMapping, bw);
				mapSqlParameterSource.addValue(INCREMENTED_VERSION, incrementedVersionVal, Types.INTEGER);
			} else {
				PropertyMapping propMapping = tableMapping.getPropertyMappingByPropertyName(paramName);
				Integer columnSqlType = propMapping.getEffectiveSqlType();
				if (propMapping.isBinaryLargeObject()) {
					InternalUtils.assignBlobMapSqlParameterSource(bw, mapSqlParameterSource, propMapping, columnSqlType,
							false);
				} else if (propMapping.isCharacterLargeObject()) {
					InternalUtils.assignClobMapSqlParameterSource(bw, mapSqlParameterSource, propMapping, columnSqlType,
							false);
				} else if (propMapping.isEnum()) {
					InternalUtils.assignEnumMapSqlParameterSource(bw, mapSqlParameterSource, propMapping, columnSqlType,
							false);
				} else {
					mapSqlParameterSource.addValue(paramName, bw.getPropertyValue(paramName), columnSqlType);
				}
			}
		}
		return mapSqlParameterSource;
	}

	private Integer getIncrementedVersionValue(TableMapping tableMapping, BeanWrapper bw) {
		Integer versionVal = (Integer) bw.getPropertyValue(tableMapping.getVersionPropertyMapping().getPropertyName());
		if (versionVal == null) {
			throw new MapperException(bw.getWrappedClass().getSimpleName() + "."
					+ tableMapping.getVersionPropertyMapping().getPropertyName()
					+ " is configured with annotation @Version. Property "
					+ tableMapping.getVersionPropertyMapping().getPropertyName() + " must not be null when updating.");
		}
		return versionVal + 1;
	}

	private List<String> getIgnoreProperties(TableMapping tableMapping) {
		List<String> ignoreProps = new ArrayList<>();
		ignoreProps.add(tableMapping.getIdPropertyName());
		PropertyMapping createdOnPropMapping = tableMapping.getCreatedOnPropertyMapping();
		if (createdOnPropMapping != null) {
			ignoreProps.add(createdOnPropMapping.getPropertyName());
		}
		PropertyMapping createdByPropMapping = tableMapping.getCreatedByPropertyMapping();
		if (createdByPropMapping != null) {
			ignoreProps.add(createdByPropMapping.getPropertyName());
		}
		return ignoreProps;
	}

	private List<String> getAutoAssignProperties(TableMapping tableMapping) {
		List<String> list = new ArrayList<>();
		PropertyMapping updatedOnPropMapping = tableMapping.getUpdatedOnPropertyMapping();
		if (updatedOnPropMapping != null) {
			list.add(updatedOnPropMapping.getPropertyName());
		}
		PropertyMapping updatedByPropMapping = tableMapping.getUpdatedByPropertyMapping();
		if (updatedByPropMapping != null) {
			list.add(updatedByPropMapping.getPropertyName());
		}
		PropertyMapping versionPropMapping = tableMapping.getVersionPropertyMapping();
		if (versionPropMapping != null) {
			list.add(versionPropMapping.getPropertyName());
		}
		return list;
	}

	private SqlAndParams buildSqlAndParamsForUpdate(TableMapping tableMapping) {
		Assert.notNull(tableMapping, "tableMapping must not be null");
		List<String> propertyList = tableMapping.getPropertyMappings().stream().map(pm -> pm.getPropertyName())
				.collect(Collectors.toList());
		List<String> ignoreProps = getIgnoreProperties(tableMapping);
		propertyList.removeAll(ignoreProps);
		return buildSqlAndParams(tableMapping, propertyList);
	}

	private SqlAndParams buildSqlAndParamsForUpdateSpecificProperties(TableMapping tableMapping,
			String... propertyNames) {
		Assert.notNull(tableMapping, "tableMapping must not be null");
		Assert.notNull(propertyNames, "propertyNames must not be null");
		validateUpdateSpecificProperties(tableMapping, propertyNames);
		List<String> propertyList = new ArrayList<>(Arrays.asList(propertyNames));
		propertyList.addAll(getAutoAssignProperties(tableMapping));
		return buildSqlAndParams(tableMapping, propertyList);
	}

	private SqlAndParams buildSqlAndParams(TableMapping tableMapping, List<String> propertyList) {
		Assert.notNull(tableMapping, "tableMapping must not be null");
		Assert.notNull(propertyList, "propertyList must not be null");
		Set<String> params = new HashSet<>();
		StringBuilder sql = new StringBuilder(256);
		sql.append("UPDATE ").append(tableMapping.fullyQualifiedTableName()).append(" SET ");
		boolean first = true;
		PropertyMapping versionPropMapping = null;
		for (String propertyName : propertyList) {
			PropertyMapping propMapping = tableMapping.getPropertyMappingByPropertyName(propertyName);
			if (!first) {
				sql.append(", ");
			} else {
				first = false;
			}
			sql.append(propMapping.getColumnName());
			sql.append(" = :");

			if (propMapping.isVersionAnnotation()) {
				sql.append(INCREMENTED_VERSION);
				params.add(INCREMENTED_VERSION);
				versionPropMapping = propMapping;
			} else {
				sql.append(propMapping.getPropertyName());
				params.add(propMapping.getPropertyName());
			}
		}
		sql.append(" WHERE ").append(tableMapping.getIdColumnName()).append(" = :")
				.append(tableMapping.getIdPropertyName());
		params.add(tableMapping.getIdPropertyName());
		if (versionPropMapping != null) {
			sql.append(" AND ").append(versionPropMapping.getColumnName()).append(" = :")
					.append(versionPropMapping.getPropertyName());
			params.add(versionPropMapping.getPropertyName());
		}
		return new SqlAndParams(sql.toString(), params);
	}

	private void validateUpdateSpecificProperties(TableMapping tableMapping, String... propertyNames) {
		for (String propertyName : propertyNames) {
			PropertyMapping propertyMapping = tableMapping.getPropertyMappingByPropertyName(propertyName);
			if (propertyMapping == null) {
				throw new MapperException("No mapping found for property '" + propertyName + "' in class "
						+ tableMapping.getMappedObjClassName());
			}
			if (propertyMapping.isIdAnnotation()) {
				throw new MapperException("Id property " + tableMapping.getMappedObjClassName() + "." + propertyName
						+ " cannot be updated using updateSpecificProperties() method.");
			}
			if (propertyMapping.isCreatedByAnnotation() || propertyMapping.isCreatedOnAnnotation()
					|| propertyMapping.isUpdatedByAnnotation() || propertyMapping.isUpdatedOnAnnotation()
					|| propertyMapping.isVersionAnnotation()) {
				throw new MapperException("Auto assign property " + tableMapping.getMappedObjClassName() + "."
						+ propertyName + " cannot be updated using updateSpecificProperties() method.");
			}
		}
	}

	private String getUpdateSpecificPropertiesCacheKey(Object object, String[] propertyNames) {
		if (propertyNames.length > CACHEABLE_UPDATE_SPECIFIC_PROPERTIES_COUNT) {
			return null;
		} else {
			return object.getClass().getName() + "-" + String.join("-", propertyNames);
		}
	}

}