JavaTypeToSqlTypeMapping.java
package org.codefilarete.stalactite.sql.ddl;
import javax.annotation.Nullable;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Stream;
import org.codefilarete.tool.bean.InterfaceIterator;
import org.codefilarete.tool.collection.Iterables;
import static org.codefilarete.tool.Nullable.nullable;
/**
* A storage for mapping between Java classes and Sql Types. Aimed at generating schema, not reading nor writing to
* ResultSet/Statement.
* A default registry is implemented through {@link DefaultTypeMapping}.
*
* @author Guillaume Mary
* @see #getTypeName(Class)
*/
public class JavaTypeToSqlTypeMapping {
/**
* SQL types storage per Java type, dedicated to sized-types.
* Values are {@link SortedMap}s of size to SQL type. {@link SortedMap} are used to ease finding of types per size
*/
private final Map<Class, SQLTypeHolder> javaTypeToSQLType = new HashMap<>(50);
/**
* Registers a Java class to a SQL type mapping
*
* @param clazz the Java class to bind
* @param sqlType the SQL type to map on the Java type
* @see #with(Class, String)
*/
public void put(Class clazz, String sqlType) {
put(clazz, sqlType, null);
}
/**
* Register a Java class to a SQL type mapping
*
* @param clazz the Java class to bind
* @param sqlType the SQL type to map on the Java type
* @param size the maximum size until which the SQL type will be used
* @see #with(Class, Size, String)
*/
public void put(Class clazz, String sqlType, Size size) {
javaTypeToSQLType.computeIfAbsent(clazz, k -> new SQLTypeHolder()).setType(sqlType, size);
}
/**
* Same as {@link #put(Class, String)} with fluent writing
*
* @param clazz the Java class to bind
* @param sqlType the SQL type to map on the Java type
* @return this
*/
public JavaTypeToSqlTypeMapping with(Class clazz, String sqlType) {
put(clazz, sqlType);
return this;
}
/**
* Same as {@link #put(Class, String, Size)} with fluent writing
*
* @param clazz the Java class to bind
* @param size the minimal size from which the SQL type will be used
* @param sqlType the SQL type to map on the Java type
* @return this
*/
public JavaTypeToSqlTypeMapping with(Class clazz, Size size, String sqlType) {
put(clazz, sqlType, size);
return this;
}
public void replace(Class clazz, String sqlType) {
replace(clazz, sqlType, null);
}
public void replace(Class clazz, String sqlType, Size size) {
SQLTypeHolder replacingValue = new SQLTypeHolder();
replacingValue.setType(sqlType, size);
javaTypeToSQLType.put(clazz, replacingValue);
}
/**
* Gives the SQL type of a Java class, checks also for its interfaces
*
* @param javaType a Java class
* @return the SQL type for the given column or null if not found
*/
public String getTypeName(Class javaType) {
return getTypeName(javaType, null);
}
/**
* Gives the nearest SQL type of a Java class according to the expected size
*
* @param javaType a Java class
* @return the SQL type for the given column
*/
public String getTypeName(Class javaType, @Nullable Size size) {
SQLTypeHolder type = javaTypeToSQLType.get(javaType);
if (type == null) {
if (javaType.isEnum()) {
type = javaTypeToSQLType.get(Enum.class);
} else {
InterfaceIterator interfaceIterator = new InterfaceIterator(javaType);
Stream<SQLTypeHolder> stream = Iterables.stream(interfaceIterator).map(javaTypeToSQLType::get);
type = stream.filter(Objects::nonNull).findFirst().orElse(null);
}
}
return nullable(type).map(dataType -> dataType.getType(size)).getOr((String) null);
}
private static class SQLTypeHolder {
private final SortedMap<Size, String> availableSizes = new TreeMap<>(Comparator.nullsLast((size1, size2) -> {
if (size1 == size2) {
return 0;
}
if (size1 == null) {
return -1;
}
if (size2 == null) {
return 1;
}
if (size1 instanceof Length && size2 instanceof Length) {
return Integer.compare(((Length) size1).getValue(), ((Length) size2).getValue());
} else if (size1 instanceof FixedPoint && size2 instanceof FixedPoint) {
// return -1 to make new one replace the existing one. Hack.
// because there's no reason to compare FixedPoint between them as for Length since "decimal(7, 3)" is not lower than "decimal(10)"
return -1;
} else {
// If comparing different types, you might want to define a consistent ordering
return size1.getClass().getName().compareTo(size2.getClass().getName());
}
}));
public String getType(@Nullable Size size) {
if (size == null) {
return getTypeForLength(null);
} else {
return getTypeForLength(size);
}
}
public void setType(String sqlTypeName, @Nullable Size size) {
availableSizes.put(size, sqlTypeName);
}
private String getTypeForLength(@Nullable Size size) {
String typeName;
if (size == null && availableSizes.size() == 1) {
Entry<Size, String> lonelyEntry = Iterables.first(availableSizes);
typeName = lonelyEntry.getValue();
size = lonelyEntry.getKey();
} else {
SortedMap<Size, String> typeNames = availableSizes.tailMap(size);
typeName = Iterables.firstValue(typeNames);
}
if (typeName != null) {
if (size instanceof Length) {
// NB: we use $l as Hibernate to ease an eventual switch between frameworks
typeName = typeName.replace("$l", String.valueOf(((Length) size).getValue()));
} else if (size instanceof FixedPoint) {
typeName = typeName.replace("$p", String.valueOf(((FixedPoint) size).getPrecision()));
typeName = typeName.replace("$s", String.valueOf(((FixedPoint) size).getScale()));
}
}
return typeName;
}
}
}