PandaLib/pandalib-db/src/main/java/fr/pandacube/lib/db/SQLElement.java

804 lines
33 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package fr.pandacube.lib.db;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import fr.pandacube.lib.util.EnumUtil;
import fr.pandacube.lib.util.Log;
/**
* Represents an entry in a SQL table. Each subclass is for a specific table.
* @param <E> the type of the subclass.
*/// TODO exemple subclass
public abstract class SQLElement<E extends SQLElement<E>> {
// cache for fields for each subclass of SQLElement
/* package */ static final Map<Class<? extends SQLElement<?>>, SQLFieldMap<? extends SQLElement<?>>> fieldsCache = new HashMap<>();
/* package */ static class SQLFieldMap<E extends SQLElement<E>> extends LinkedHashMap<String, SQLField<E, ?>> {
private final Class<E> sqlElemClass;
private SQLFieldMap(Class<E> elemClass) {
sqlElemClass = elemClass;
}
private void addField(SQLField<?, ?> f) {
if (f == null) return;
if (containsKey(f.getName())) throw new IllegalArgumentException(
"SQLField " + f.getName() + " already exist in " + sqlElemClass.getName());
@SuppressWarnings("unchecked")
SQLField<E, ?> checkedF = (SQLField<E, ?>) f;
checkedF.setSQLElementType(sqlElemClass);
put(checkedF.getName(), checkedF);
}
}
private final DBConnection db = DB.getConnection();
private boolean stored = false;
private int id;
private final SQLFieldMap<E> fields;
private final Map<SQLField<E, ?>, Object> values;
/* package */ final Set<String> modifiedSinceLastSave;
/**
* Create a new instance of a table entry, not yet saved in the database.
* All the required values has to be set before saving the entry in the database.
*/
@SuppressWarnings("unchecked")
protected SQLElement() {
try {
DB.initTable((Class<E>)getClass());
} catch (DBInitTableException e) {
throw new RuntimeException(e);
}
if (fieldsCache.get(getClass()) == null) {
fields = new SQLFieldMap<>(getCheckedClass());
// le champ id commun à toutes les tables
SQLField<E, Integer> idF = new SQLField<>(INT, false, true, 0);
idF.setName("id");
fields.addField(idF);
generateFields(fields);
fieldsCache.put(getCheckedClass(), fields);
}
else
fields = (SQLFieldMap<E>) fieldsCache.get(getClass());
values = new LinkedHashMap<>(fields.size());
modifiedSinceLastSave = new HashSet<>(fields.size());
initDefaultValues();
}
/**
* Create a new instance of a table entry, representing an already present one in the database.
* <p>
* Subclasses must implement a constructor with the same signature, that calls this parent constructor, and may be
* private to avoid accidental instanciation. This constructor will be called by the DB API when fetching entries
* from the database.
* @param id the id of the entry in the database.
*/
protected SQLElement(int id) {
this();
@SuppressWarnings("unchecked")
SQLField<E, Integer> idField = (SQLField<E, Integer>) fields.get("id");
set(idField, id, false);
this.id = id;
stored = true;
}
/**
* Gets the name of the table in the database, without the prefix defined by {@link DB#init(DBConnection, String)}.
* @return The non-prefixed name of the table in the database.
*/
protected abstract String tableName();
/**
* Gets a checked version of the {@link Class} instance of this {@link SQLElement} object.
* @return {@code (Class<E>) getClass()};
*/
@SuppressWarnings("unchecked")
public Class<E> getCheckedClass() {
return (Class<E>) getClass();
}
/**
* Fills the entries values that are known to be nullable or have a default value.
*/
@SuppressWarnings("unchecked")
private void initDefaultValues() {
for (@SuppressWarnings("rawtypes")
SQLField f : fields.values())
if (f.defaultValue != null) set(f, f.defaultValue);
else if (f.nullable || (f.autoIncrement && !stored)) set(f, null);
}
@SuppressWarnings("unchecked")
private void generateFields(SQLFieldMap<E> listToFill) {
java.lang.reflect.Field[] declaredFields = getClass().getDeclaredFields();
for (java.lang.reflect.Field field : declaredFields) {
if (!SQLField.class.isAssignableFrom(field.getType())) {
Log.debug("[ORM] The field " + field.getDeclaringClass().getName() + "." + field.getName() + " is of type " + field.getType().getName() + " so it will be ignored.");
continue;
}
if (!Modifier.isStatic(field.getModifiers())) {
Log.severe("[ORM] The field " + field.getDeclaringClass().getName() + "." + field.getName() + " can't be initialized because it is not static.");
continue;
}
field.setAccessible(true);
try {
Object val = field.get(null);
if (!(val instanceof SQLField)) {
Log.severe("[ORM] The field " + field.getDeclaringClass().getName() + "." + field.getName() + " can't be initialized because its value is null.");
continue;
}
SQLField<E, ?> checkedF = (SQLField<E, ?>) val;
checkedF.setName(field.getName());
if (!Modifier.isPublic(field.getModifiers()))
Log.warning("[ORM] The field " + field.getDeclaringClass().getName() + "." + field.getName() + " should be public !");
if (listToFill.containsKey(checkedF.getName())) throw new IllegalArgumentException(
"SQLField " + checkedF.getName() + " already exist in " + getClass().getName());
checkedF.setSQLElementType((Class<E>) getClass());
listToFill.addField((SQLField<?, ?>) val);
} catch (IllegalArgumentException | IllegalAccessException e) {
Log.severe("Can't get value of static field " + field, e);
}
}
}
/* package */ Map<String, SQLField<E, ?>> getFields() {
return Collections.unmodifiableMap(fields);
}
/**
* Gets the fiels of this entrys table, mapped to the values of this entry.
* @return the fiels of this entrys table, mapped to the values of this entry.
*/
public Map<SQLField<E, ?>, Object> getValues() {
return Collections.unmodifiableMap(values);
}
/**
* Sets a value in this entry.
* <p>
* This is not good practice to set the {@code id} field of any entry, because its a unique auto-incremented
* value. Use {@link #save()} and {@link #delete()} to set or unset the {@code id} instead, in consistence with the
* database.
* @param field the field to set.
* @param value the new value for this field.
* @return this.
* @param <T> the Java type of the field.
*/
@SuppressWarnings("unchecked")
public <T> E set(SQLField<E, T> field, T value) {
set(field, value, true);
return (E) this;
}
/* package */ <T> void set(SQLField<E, T> sqlField, T value, boolean setModified) {
if (sqlField == null) throw new IllegalArgumentException("sqlField can't be null");
if (!fields.containsValue(sqlField)) // should not append at runtime because of generic type check at compilation
throw new IllegalStateException("In the table "+getClass().getName()+ ": the field asked for modification is not initialized properly.");
if (value == null) {
if (!sqlField.nullable && (!sqlField.autoIncrement || stored))
throw new IllegalArgumentException(
"SQLField '" + sqlField.getName() + "' of " + getClass().getName() + " is a NOT NULL field");
}
else if (!sqlField.type.isInstance(value)) {
throw new IllegalArgumentException("SQLField '" + sqlField.getName() + "' of " + getClass().getName()
+ " type is '" + sqlField.type + "' and can't accept values of type "
+ value.getClass().getName());
}
if (!values.containsKey(sqlField)) {
values.put(sqlField, value);
if (setModified) modifiedSinceLastSave.add(sqlField.getName());
}
else {
Object oldVal = values.get(sqlField);
if (!Objects.equals(oldVal, value)) {
values.put(sqlField, value);
if (setModified) modifiedSinceLastSave.add(sqlField.getName());
}
// sinon, rien n'est modifié
}
}
/**
* Gets the value of the provided field in this entry.
* @param field the field to get the value from.
* @return the value of the provided field in this entry.
* @param <T> the Java type of the field.
*/
public <T> T get(SQLField<E, T> field) {
if (field == null) throw new IllegalArgumentException("field can't be null");
if (values.containsKey(field)) {
@SuppressWarnings("unchecked")
T val = (T) values.get(field);
return val;
}
throw new IllegalArgumentException("The field '" + field.getName() + "' in this instance of " + getClass().getName()
+ " does not exist or is not set");
}
/**
* Gets the foreign table entry targeted by the provided foreign key of this table.
* @param field a foreign key of this table.
* @param <T> the type of the foreign key field.
* @param <P> the targeted foreign table type.
* @return the foreign table entry targeted by the provided foreign key of this table.
* @throws DBException if an error occurs when interacting with the database.
*/
public <T, P extends SQLElement<P>> P getReferencedEntry(SQLFKField<E, T, P> field) throws DBException {
T fkValue = get(field);
if (fkValue == null) return null;
return DB.getFirst(field.getForeignElementClass(), field.getPrimaryField().eq(fkValue), null);
}
/**
* Gets the original table entry which the provided foreign key is targeting this entry, and following the provided
* {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses.
* @param field a foreign key in the original table.
* @param orderBy the {@code ORDER BY} clause of the query.
* @param limit the {@code LIMIT} clause of the query.
* @param offset the {@code OFFSET} clause of the query.
* @param <T> the type of the foreign key field.
* @param <F> the table class of the foreign key that reference a field of this entry.
* @return the original table entry which the provided foreign key is targeting this entry.
* @throws DBException if an error occurs when interacting with the database.
*/
public <T, F extends SQLElement<F>> SQLElementList<F> getReferencingForeignEntries(SQLFKField<F, T, E> field, SQLOrderBy<F> orderBy, Integer limit, Integer offset) throws DBException {
T value = get(field.getPrimaryField());
if (value == null) return new SQLElementList<>();
return DB.getAll(field.getSQLElementType(), field.eq(value), orderBy, limit, offset);
}
/**
* Determine if this entry is valid for save, that is when all the values are either set, or are nullable or have a
* default value.
* @return true if this entry is valid for save, false otherwise.
*/
public boolean isValidForSave() {
return values.keySet().containsAll(fields.values());
}
private Map<SQLField<E, ?>, Object> getOnlyModifiedValues() {
Map<SQLField<E, ?>, Object> modifiedValues = new LinkedHashMap<>();
values.forEach((k, v) -> {
if (modifiedSinceLastSave.contains(k.getName())) modifiedValues.put(k, v);
});
return modifiedValues;
}
/**
* Determine if the provided entry has been modified since this entry was created or saved.
* @param field the field to check in this entry.
* @return true if the field has been modified, false otherwise.
*/
public boolean isModified(SQLField<E, ?> field) {
return modifiedSinceLastSave.contains(field.getName());
}
/**
* Saves this entry into the database, either by updating the already existing entry in it, or by creating a new
* entry if it doesn't exist yet.
* @return this.
* @throws DBException if an error occurs when interacting with the database.
*/
@SuppressWarnings("unchecked")
public E save() throws DBException {
if (!isValidForSave())
throw new IllegalStateException(this + " has at least one undefined value and can't be saved.");
DB.initTable((Class<E>)getClass());
try {
if (stored) { // update in database
// restore the id field to its real value in case it was modified using #set(...)
values.put(fields.get("id"), id);
modifiedSinceLastSave.remove("id");
Map<SQLField<E, ?>, Object> modifiedValues = getOnlyModifiedValues();
if (modifiedValues.isEmpty()) return (E) this;
DB.update((Class<E>)getClass(), getIdField().eq(getId()), modifiedValues);
}
else { // add entry in the database
// restore the id field to its real value in case it was modified using #set(...)
values.put(fields.get("id"), null);
StringBuilder concatValues = new StringBuilder();
StringBuilder concatFields = new StringBuilder();
List<Object> psValues = new ArrayList<>();
boolean first = true;
for (Map.Entry<SQLField<E, ?>, Object> entry : values.entrySet()) {
if (!first) {
concatValues.append(",");
concatFields.append(",");
}
first = false;
concatValues.append(" ? ");
concatFields.append("`").append(entry.getKey().getName()).append("`");
addValueToSQLObjectList(psValues, entry.getKey(), entry.getValue());
}
try (Connection c = db.getConnection();
PreparedStatement ps = c.prepareStatement("INSERT INTO " + DB.tablePrefix + tableName() + " (" + concatFields + ") VALUES (" + concatValues + ")",
Statement.RETURN_GENERATED_KEYS)) {
int i = 1;
for (Object val : psValues)
ps.setObject(i++, val);
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) id = rs.getInt(1);
stored = true;
}
}
}
modifiedSinceLastSave.clear();
} catch (SQLException e) {
throw new DBException("Error while saving data", e);
}
return (E) this;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
/* package */ static <E extends SQLElement<E>> void addValueToSQLObjectList(List<Object> list, SQLField<E, ?> field, Object jValue) throws DBException {
if (jValue != null && field.type instanceof SQLCustomType) {
try {
jValue = ((SQLCustomType)field.type).javaToDbConv.apply(jValue);
} catch (Exception e) {
throw new DBException("Error while converting value of field '"+field.getName()+"' with SQLCustomType from "+field.type.getJavaType()
+"(java source) to "+((SQLCustomType<?, ?>)field.type).intermediateJavaType+"(jdbc destination). The original value is '"+jValue+"'", e);
}
}
list.add(jValue);
}
/**
* Tells if this entry is currently stored in DB or not.
* @return true if this entry is currently stored in DB, or false otherwise.
*/
public boolean isStored() {
return stored;
}
/**
* Gets the id of the entry in the DB.
* @return the id of the entry in the DB, or null if its not saved.
*/
public Integer getId() {
return (stored) ? id : null;
}
/**
* Gets the {@link SQLField} instance corresponding to the {@code id} field of this table.
* @return the {@link SQLField} instance corresponding to the {@code id} field of this table.
*/
@SuppressWarnings("unchecked")
public SQLField<E, Integer> getIdField() {
return (SQLField<E, Integer>) fields.get("id");
}
/**
* Deletes this entry from the database.
* @throws DBException if an error occurs when interacting with the database.
*/
public void delete() throws DBException {
if (stored) {
DB.delete(getCheckedClass(), getIdField().eq(id));
markAsNotStored();
}
}
/* package */ void markAsNotStored() {
stored = false;
id = 0;
modifiedSinceLastSave.clear();
values.forEach((k, v) -> modifiedSinceLastSave.add(k.getName()));
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(this.getClass().getName());
sb.append('{');
sb.append(fields.values().stream()
.map(f -> {
try {
return f.getName() + "=" + get(f);
} catch (IllegalArgumentException e) {
return f.getName() + "=(Undefined)";
}
})
.collect(Collectors.joining(", ")));
sb.append('}');
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (!(getClass().isInstance(o))) return false;
SQLElement<?> oEl = (SQLElement<?>) o;
if (oEl.getId() == null) return false;
return oEl.getId().equals(getId());
}
@Override
public int hashCode() {
return getClass().hashCode() ^ Objects.hashCode(getId());
}
/**
* Creates a new SQL field.
* @param type the type of the field.
* @param nullable true if nullable, false if {@code NOT NULL}.
* @param autoIncrement if {@code AUTO_INCREMENT}.
* @param deflt a default value for this field. A null value indicate that this has no default value.
* @return the new SQL field.
* @param <E> the table type.
* @param <T> the Java type of this field.
*/
protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncrement, T deflt) {
return new SQLField<>(type, nullable, autoIncrement, deflt);
}
/**
* Creates a new SQL field.
* @param type the type of the field.
* @param nullable true if nullable, false if {@code NOT NULL}.
* @return the new SQL field.
* @param <E> the table type.
* @param <T> the Java type of this field.
*/
protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable) {
return new SQLField<>(type, nullable);
}
/**
* Creates a new SQL field.
* @param type the type of the field.
* @param nullable true if nullable, false if {@code NOT NULL}.
* @param autoIncrement if {@code AUTO_INCREMENT}.
* @return the new SQL field.
* @param <E> the table type.
* @param <T> the Java type of this field.
*/
protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncrement) {
return new SQLField<>(type, nullable, autoIncrement);
}
/**
* Creates a new SQL field.
* @param type the type of the field.
* @param nullable true if nullable, false if {@code NOT NULL}.
* @param deflt a default value for this field. A null value indicate that this has no default value.
* @return the new SQL field.
* @param <E> the table type.
* @param <T> the Java type of this field.
*/
protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, T deflt) {
return new SQLField<>(type, nullable, deflt);
}
/**
* Creates a new SQL foreign key field pointing to the {@code id} field of the provided table.
* @param nul true if this foreign key is nullable, false if {@code NOT NULL}.
* @param deflt a default value for this field. A null value indicate that this has no default value.
* @param fkEl the target table.
* @return the new SQL foreign key field.
* @param <E> the table type.
* @param <F> the target table type.
*/
protected static <E extends SQLElement<E>, F extends SQLElement<F>> SQLFKField<E, Integer, F> foreignKeyId(boolean nul, Integer deflt, Class<F> fkEl) {
return SQLFKField.idFK(nul, deflt, fkEl);
}
/**
* Creates a new SQL foreign key field pointing to the {@code id} field of the provided table.
* @param nul true if this foreign key is nullable, false if {@code NOT NULL}.
* @param fkEl the target table.
* @return the new SQL foreign key field.
* @param <E> the table type.
* @param <F> the target table type.
*/
protected static <E extends SQLElement<E>, F extends SQLElement<F>> SQLFKField<E, Integer, F> foreignKeyId(boolean nul, Class<F> fkEl) {
return SQLFKField.idFK(nul, fkEl);
}
/**
* Creates a new SQL foreign key field pointing to the provided field.
* @param nul true if this foreign key is nullable, false if {@code NOT NULL}.
* @param deflt a default value for this field. A null value indicate that this has no default value.
* @param fkEl the target table.
* @param fkF the field in the targeted table.
* @return the new SQL foreign key field.
* @param <E> the table type.
* @param <T> the Java type of this field.
* @param <F> the target table type.
*/
protected static <E extends SQLElement<E>, T, F extends SQLElement<F>> SQLFKField<E, T, F> foreignKey(boolean nul, T deflt, Class<F> fkEl, SQLField<F, T> fkF) {
return SQLFKField.customFK(nul, deflt, fkEl, fkF);
}
/**
* Creates a new SQL foreign key field pointing to the provided field.
* @param nul true if this foreign key is nullable, false if {@code NOT NULL}.
* @param fkEl the target table.
* @param fkF the field in the targeted table.
* @return the new SQL foreign key field.
* @param <E> the table type.
* @param <T> the Java type of this field.
* @param <F> the target table type.
*/
protected static <E extends SQLElement<E>, T, F extends SQLElement<F>> SQLFKField<E, T, F> foreignKey(boolean nul, Class<F> fkEl, SQLField<F, T> fkF) {
return SQLFKField.customFK(nul, fkEl, fkF);
}
// List of type from https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-type-conversions.html
/** SQL type {@code BIT(1)} represented in Java as a {@link Boolean}. */
public static final SQLType<Boolean> BIT_1 = new SQLType<>("BIT(1)", Boolean.class);
/**
* SQL type {@code BIT(bitCount)} represented in Java as a {@code byte[]}.
* @param bitCount the number of bits. At least 2, or use {@link #BIT_1} if 1 bit is needed.
* @return the SQL type {@code BIT} with the specified bitCount.
*/
public static SQLType<byte[]> BIT(int bitCount) {
if (bitCount <= 1)
throw new IllegalArgumentException("charCount must be greater than 1. If 1 is desired, use BIT_1 instead.");
return new SQLType<>("BIT(" + bitCount + ")", byte[].class);
}
/** SQL type {@code BOOLEAN} represented in Java as a {@link Boolean}. */
public static final SQLType<Boolean> BOOLEAN = new SQLType<>("BOOLEAN", Boolean.class);
/** SQL type {@code TINYINT} represented in Java as an {@link Integer}. */
public static final SQLType<Integer> TINYINT = new SQLType<>("TINYINT", Integer.class); // cant be Byte due to MYSQL JDBC Connector limitations
/**
* Alias for the SQL type {@code TINYINT} represented in Java as an {@link Integer}.
* @deprecated use {@link #TINYINT} instead.
*/
@Deprecated
public static final SQLType<Integer> BYTE = TINYINT;
/** SQL type {@code SMALLINT} represented in Java as an {@link Integer}. */
public static final SQLType<Integer> SMALLINT = new SQLType<>("SMALLINT", Integer.class); // cant be Short due to MYSQL JDBC Connector limitations
/**
* Alias for the SQL type {@code SMALLINT} represented in Java as an {@link Integer}.
* @deprecated use {@link #SMALLINT} instead.
*/
@Deprecated
public static final SQLType<Integer> SHORT = SMALLINT;
/** SQL type {@code MEDIUMINT} represented in Java as an {@link Integer}. */
public static final SQLType<Integer> MEDIUMINT = new SQLType<>("MEDIUMINT", Integer.class);
/** SQL type {@code INT} represented in Java as an {@link Integer}. */
public static final SQLType<Integer> INT = new SQLType<>("INT", Integer.class);
/**
* Alias for the SQL type {@code INT} represented in Java as an {@link Integer}.
* @deprecated use {@link #INT} instead.
*/
@Deprecated
public static final SQLType<Integer> INTEGER = INT;
/** SQL type {@code BIGINT} represented in Java as a {@link Long}. */
public static final SQLType<Long> BIGINT = new SQLType<>("BIGINT", Long.class);
/**
* Alias for the SQL type {@code BIGINT} represented in Java as a {@link Long}.
* @deprecated use {@link #BIGINT} instead.
*/
@Deprecated
public static final SQLType<Long> LONG = BIGINT;
/** SQL type {@code FLOAT} represented in Java as a {@link Float}. */
public static final SQLType<Float> FLOAT = new SQLType<>("FLOAT", Float.class);
/** SQL type {@code DOUBLE} represented in Java as a {@link Double}. */
public static final SQLType<Double> DOUBLE = new SQLType<>("DOUBLE", Double.class);
/** SQL type {@code DECIMAL} represented in Java as a {@link BigDecimal}. */
public static final SQLType<BigDecimal> DECIMAL = new SQLType<>("DECIMAL", BigDecimal.class);
/** SQL type {@code DATE} represented in Java as a {@link Date}. */
public static final SQLType<Date> DATE = new SQLType<>("DATE", Date.class);
/** SQL type {@code DATETIME} represented in Java as a {@link LocalDateTime}. */
public static final SQLType<LocalDateTime> DATETIME = new SQLType<>("DATETIME", LocalDateTime.class);
/** SQL type {@code TIMESTAMP} represented in Java as a {@link Timestamp}. */
public static final SQLType<Timestamp> TIMESTAMP = new SQLType<>("TIMESTAMP", Timestamp.class);
/** SQL type {@code TIME} represented in Java as a {@link Time}. */
public static final SQLType<Time> TIME = new SQLType<>("TIME", Time.class);
/**
* SQL type {@code CHAR(charCount)} represented in Java as a {@code String}.
* @param charCount the number of character.
* @return the SQL type {@code CHAR} with the specified charCount.
*/
public static SQLType<String> CHAR(int charCount) {
if (charCount <= 0) throw new IllegalArgumentException("charCount must be positive.");
return new SQLType<>("CHAR(" + charCount + ")", String.class);
}
/**
* SQL type {@code VARCHAR(charCount)} represented in Java as a {@code String}.
* @param charCount the number of character.
* @return the SQL type {@code VARCHAR} with the specified charCount.
*/
public static SQLType<String> VARCHAR(int charCount) {
if (charCount <= 0) throw new IllegalArgumentException("charCount must be positive.");
return new SQLType<>("VARCHAR(" + charCount + ")", String.class);
}
/**
* SQL type {@code BINARY(byteCount)} represented in Java as a {@code byte[]}.
* @param byteCount the number of bits.
* @return the SQL type {@code BINARY} with the specified byteCount.
*/
public static SQLType<byte[]> BINARY(int byteCount) {
if (byteCount <= 0) throw new IllegalArgumentException("byteCount must be positive.");
return new SQLType<>("BINARY(" + byteCount + ")", byte[].class);
}
/**
* SQL type {@code VARBINARY(byteCount)} represented in Java as a {@code byte[]}.
* @param byteCount the number of bits.
* @return the SQL type {@code VARBINARY} with the specified byteCount.
*/
public static SQLType<byte[]> VARBINARY(int byteCount) {
if (byteCount <= 0) throw new IllegalArgumentException("byteCount must be positive.");
return new SQLType<>("VARBINARY(" + byteCount + ")", byte[].class);
}
/** SQL type {@code BLOB} represented in Java as a {@link byte[]}. */
public static final SQLType<byte[]> BLOB = new SQLType<>("BLOB", byte[].class);
/** SQL type {@code TINYBLOB} represented in Java as a {@link byte[]}. */
public static final SQLType<byte[]> TINYBLOB = new SQLType<>("TINYBLOB", byte[].class);
/** SQL type {@code MEDIUMBLOB} represented in Java as a {@link byte[]}. */
public static final SQLType<byte[]> MEDIUMBLOB = new SQLType<>("MEDIUMBLOB", byte[].class);
/** SQL type {@code LONGBLOB} represented in Java as a {@link byte[]}. */
public static final SQLType<byte[]> LONGBLOB = new SQLType<>("LONGBLOB", byte[].class);
/** SQL type {@code TEXT} represented in Java as a {@link String}. */
public static final SQLType<String> TEXT = new SQLType<>("TEXT", String.class);
/**
* Alias for the SQL type {@code TEXT} represented in Java as a {@link String}.
* @deprecated use {@link #TEXT} instead.
*/
@Deprecated
public static final SQLType<String> STRING = TEXT;
/** SQL type {@code TINYTEXT} represented in Java as a {@link String}. */
public static final SQLType<String> TINYTEXT = new SQLType<>("TINYTEXT", String.class);
/** SQL type {@code MEDIUMTEXT} represented in Java as a {@link String}. */
public static final SQLType<String> MEDIUMTEXT = new SQLType<>("MEDIUMTEXT", String.class);
/** SQL type {@code LONGTEXT} represented in Java as a {@link String}. */
public static final SQLType<String> LONGTEXT = new SQLType<>("LONGTEXT", String.class);
/**
* SQL type {@code ENUM(...)} represented in Java as an enum value.
* @param enumType the enum type.
* @return the SQL type {@code ENUM} representing the provided enum.
* @param <T> the type of the enum.
*/
public static <T extends Enum<T>> SQLType<T> ENUM(Class<T> enumType) {
if (enumType == null) throw new IllegalArgumentException("enumType can't be null.");
StringBuilder enumStr = new StringBuilder("'");
boolean first = true;
for (T el : enumType.getEnumConstants()) {
if (!first) enumStr.append("', '");
first = false;
enumStr.append(el.name());
}
enumStr.append("'");
return new SQLCustomType<>("ENUM(" + enumStr + ")", String.class, enumType, s -> EnumUtil.searchEnum(enumType, s), Enum::name);
}
/**
* SQL type {@code SET(...)} represented in Java as an {@link EnumSet} of the specified enum type.
* @param enumType the enum type.
* @return the SQL type {@code SET} representing the provided enum.
* @param <T> the type of the enum.
*/
public static <T extends Enum<T>> SQLType<EnumSet<T>> SET(Class<T> enumType) {
if (enumType == null) throw new IllegalArgumentException("enumType can't be null.");
StringBuilder enumStr = new StringBuilder("'");
boolean first = true;
for (T el : enumType.getEnumConstants()) {
if (!first) enumStr.append("', '");
first = false;
enumStr.append(el.name());
}
enumStr.append("'");
@SuppressWarnings("unchecked")
Class<EnumSet<T>> enumSetType = (Class<EnumSet<T>>) EnumSet.noneOf(enumType).getClass();
return new SQLCustomType<>("SET(" + enumStr + ")", String.class, enumSetType,
s -> Arrays.stream(s.split(","))
.map(strV -> EnumUtil.searchEnum(enumType, strV))
.filter(Objects::nonNull)
.collect(Collectors.toCollection(() -> EnumSet.noneOf(enumType))),
set -> set.stream()
.map(Enum::name)
.collect(Collectors.joining(",")));
}
/** A custom type based on SQL type {@code CHAR(36)} represented in Java as a {@link UUID}. */
public static final SQLType<UUID> CHAR36_UUID = new SQLCustomType<>(CHAR(36), UUID.class, UUID::fromString, UUID::toString);
}