diff --git a/hibernate-types-43/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java b/hibernate-types-43/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java index c7c984b1d..2a3e60e75 100644 --- a/hibernate-types-43/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java +++ b/hibernate-types-43/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java @@ -140,7 +140,7 @@ public static class Book { private String isbn; @Type(type = "json") - @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT IS_VALID_JSON CHECK (properties IS JSON)") + @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT HT_BOOK_IS_VALID_JSON CHECK (properties IS JSON)") private String properties; public String getIsbn() { diff --git a/hibernate-types-43/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java b/hibernate-types-43/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java index e11e31542..86aff98fb 100644 --- a/hibernate-types-43/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java +++ b/hibernate-types-43/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java @@ -86,7 +86,7 @@ public static class Book { private String isbn; @Type(type = "json") - @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT IS_VALID_JSON CHECK (properties IS JSON)") + @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT HT_BOOK_IS_VALID_JSON CHECK (properties IS JSON)") private Map properties = new HashMap(); public String getIsbn() { diff --git a/hibernate-types-5/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java b/hibernate-types-5/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java index 92f346046..e76d61873 100644 --- a/hibernate-types-5/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java +++ b/hibernate-types-5/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java @@ -136,7 +136,7 @@ public static class Book { private String isbn; @Type(type = "json") - @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT IS_VALID_JSON CHECK (properties IS JSON)") + @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT HT_BOOK_IS_VALID_JSON CHECK (properties IS JSON)") private String properties; public String getIsbn() { diff --git a/hibernate-types-5/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java b/hibernate-types-5/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java index b2ed24198..82fdec244 100644 --- a/hibernate-types-5/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java +++ b/hibernate-types-5/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java @@ -86,7 +86,7 @@ public static class Book { private String isbn; @Type(type = "json") - @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT IS_VALID_JSON CHECK (properties IS JSON)") + @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT HT_BOOK_IS_VALID_JSON CHECK (properties IS JSON)") private Map properties = new HashMap(); public String getIsbn() { diff --git a/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java b/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java index 9d57c7150..39f3cd536 100644 --- a/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java +++ b/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java @@ -145,7 +145,7 @@ public static class Book { private String isbn; @Type(type = "json") - @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT IS_VALID_JSON CHECK (properties IS JSON)") + @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT HT_BOOK_IS_VALID_JSON CHECK (properties IS JSON)") private String properties; public String getIsbn() { diff --git a/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java b/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java index e39c355af..d584c5ad3 100644 --- a/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java +++ b/hibernate-types-52/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java @@ -73,7 +73,7 @@ public static class Book { private String isbn; @Type(type = "json") - @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT IS_VALID_JSON CHECK (properties IS JSON)") + @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT HT_BOOK_IS_VALID_JSON CHECK (properties IS JSON)") private Map properties = new HashMap<>(); public String getIsbn() { diff --git a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java index 59bf35a1e..fbae9c5eb 100644 --- a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java +++ b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java @@ -145,7 +145,7 @@ public static class Book { private String isbn; @Type(type = "json") - @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT IS_VALID_JSON CHECK (properties IS JSON)") + @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT HT_BOOK_IS_VALID_JSON CHECK (properties IS JSON)") private String properties; public String getIsbn() { diff --git a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java index e39c355af..d584c5ad3 100644 --- a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java +++ b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java @@ -73,7 +73,7 @@ public static class Book { private String isbn; @Type(type = "json") - @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT IS_VALID_JSON CHECK (properties IS JSON)") + @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT HT_BOOK_IS_VALID_JSON CHECK (properties IS JSON)") private Map properties = new HashMap<>(); public String getIsbn() { diff --git a/hibernate-types-60/pom.xml b/hibernate-types-60/pom.xml new file mode 100644 index 000000000..1b5e9e6ff --- /dev/null +++ b/hibernate-types-60/pom.xml @@ -0,0 +1,129 @@ + + + + com.vladmihalcea + hibernate-types-parent + 2.14.2-SNAPSHOT + + + 4.0.0 + + hibernate-types-60 + 2.14.2-SNAPSHOT + jar + + hibernate-types-60 + Hibernate ORM 6.0 extra Types + + + + + org.hibernate + hibernate-core + ${hibernate.version} + provided + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + provided + true + + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + ${jackson.version} + + + + com.google.guava + guava + ${guava.version} + provided + true + + + + org.hibernate + hibernate-jcache + ${hibernate.version} + test + + + + org.postgresql + postgresql + ${postgresql.version} + provided + true + + + + javax.xml.bind + jaxb-api + 2.3.0 + provided + true + + + + + + 11 + + 6.0.0.Final + 42.3.3 + + 8.0.28 + 2.11.0 + 29.0-jre + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${jdk.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + ${env.JAVA_HOME_11}/bin/java + + **/*JvmForkTest.java + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + com.vladmihalcea.hibernate.type + + + + + + + test-jar + + + + + + + + diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/id/BatchSequenceGenerator.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/id/BatchSequenceGenerator.java new file mode 100644 index 000000000..cf1f4ef68 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/id/BatchSequenceGenerator.java @@ -0,0 +1,404 @@ +package com.vladmihalcea.hibernate.id; + +import org.hibernate.HibernateException; +import org.hibernate.MappingException; +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.model.relational.QualifiedNameParser; +import org.hibernate.boot.model.relational.SqlStringGenerationContext; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.jdbc.spi.JdbcCoordinator; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.BulkInsertionCapableIdentifierGenerator; +import org.hibernate.id.Configurable; +import org.hibernate.id.IdentifierGenerationException; +import org.hibernate.id.PersistentIdentifierGenerator; +import org.hibernate.id.enhanced.DatabaseStructure; +import org.hibernate.id.enhanced.Optimizer; +import org.hibernate.id.enhanced.SequenceStructure; +import org.hibernate.internal.util.config.ConfigurationHelper; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.Type; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * A sequence generator that uses a recursive query to fetch multiple + * values from a sequence in a single database access. + * + *

Parameters

+ * The following configuration parameters are supported: + *
+ *
{@value #SEQUENCE_PARAM}
+ *
mandatory, name of the sequence to use
+ *
{@value #FETCH_SIZE_PARAM}
+ *
optional, how many sequence numbers should be fetched at a time, + * default is {@value #DEFAULT_FETCH_SIZE}
+ *
+ * + *

SQL

+ * Per default the generated SELECT will look something like this + *

+ * WITH RECURSIVE t(n, level_num) AS (
+ *   SELECT nextval(seq_xxx) AS n, 1 AS level_num
+ *     UNION ALL
+ *   SELECT nextval(seq_xxx) AS n, level_num + 1 AS level_num
+ *   FROM t
+ *   WHERE level_num < ?)
+ * SELECT n
+ *   FROM t;
+ * 
+ * + *

DB2

+ * For DB2 the generated SELECT will look something like this + *

+ * WITH t(n) AS (
+ *   SELECT 1 AS n
+ *     FROM (VALUES 1)
+ *       UNION ALL
+ *     SELECT n + 1 AS n
+ *       FROM t
+ *      WHERE n < ?)
+ * SELECT next value for SEQ_CHILD_ID AS n
+ *   FROM t;
+ * 
+ * + *

HSQLDB

+ * For HSQLDB the generated SELECT will look something like this + *

+ * SELECT next value for seq_parent_id
+ *   FROM UNNEST(SEQUENCE_ARRAY(1, ?, 1));
+ * 
+ * + *

Oracle

+ * For Oracle the generated SELECT will look something like this + * because Oracle does not support using recursive common table + * expressions to fetch multiple values from a sequence. + *

+ * SELECT seq_xxx.nextval
+ * FROM dual
+ * CONNECT BY rownum <= ?
+ * 
+ * + *

SQL Server

+ * For SQL Server the generated SELECT will look something like this + *

+ * WITH t(n) AS (
+ *   SELECT 1 AS n
+ *     UNION ALL
+ *   SELECT n + 1 AS n
+ *     FROM t
+ *    WHERE n < ?)
+ * SELECT NEXT VALUE FOR seq_xxx AS n
+ *   FROM t
+ * 
+ * + *

Firebird

+ * For Firebird the generated SELECT will look something like this + *

+ * WITH RECURSIVE t(n, level_num) AS (
+ *   SELECT NEXT VALUE FOR seq_xxx AS n, 1 AS level_num
+ *   FROM rdb$database
+ *     UNION ALL
+ *   SELECT NEXT VALUE FOR seq_xxx AS n, level_num + 1 AS level_num
+ *     FROM t
+ *    WHERE level_num < ?)
+ * SELECT n
+ *   FROM t
+ * 
+ * + *

Database Support

+ * The following RDBMS have been verified to work + *
    + *
  • DB2
  • + *
  • Firebird
  • + *
  • Oracle
  • + *
  • H2
  • + *
  • HSQLDB
  • + *
  • MariaDB
  • + *
  • Postgres
  • + *
  • SQL Sever
  • + *
+ *

+ * In theory any RDBMS that supports {@code WITH RECURSIVE} and + * sequences is supported. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Philippe Marschall + * @since 2.14.0 + */ +public class BatchSequenceGenerator implements BulkInsertionCapableIdentifierGenerator, PersistentIdentifierGenerator, Configurable { + + /** + * Indicates the name of the sequence to use, mandatory. + */ + public static final String SEQUENCE_PARAM = "sequence"; + + /** + * Indicates how many sequence values to fetch at once. The default value is {@link #DEFAULT_FETCH_SIZE}. + */ + public static final String FETCH_SIZE_PARAM = "fetch_size"; + + /** + * The default value for {@link #FETCH_SIZE_PARAM}. + */ + public static final int DEFAULT_FETCH_SIZE = 10; + + private final Lock lock = new ReentrantLock(); + + private String select; + private int fetchSize; + private IdentifierPool identifierPool; + private IdentifierExtractor identifierExtractor; + private DatabaseStructure databaseStructure; + + @Override + public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) + throws MappingException { + + JdbcEnvironment jdbcEnvironment = serviceRegistry.getService(JdbcEnvironment.class); + Dialect dialect = jdbcEnvironment.getDialect(); + String sequenceName = determineSequenceName(params); + this.fetchSize = determineFetchSize(params); + + this.select = buildSelect(sequenceName, dialect); + this.identifierExtractor = IdentifierExtractor.getIdentifierExtractor(type.getReturnedClass()); + this.identifierPool = IdentifierPool.empty(); + + this.databaseStructure = this.buildDatabaseStructure(type, sequenceName, jdbcEnvironment); + } + + private static String buildSelect(String sequenceName, Dialect dialect) { + if (dialect instanceof org.hibernate.dialect.Oracle8iDialect) { + return "SELECT " + dialect.getSequenceSupport().getSelectSequenceNextValString(sequenceName) + " FROM dual CONNECT BY rownum <= ?"; + } + if (dialect instanceof org.hibernate.dialect.SQLServerDialect) { + // No RECURSIVE + return "WITH t(n) AS ( " + + "SELECT 1 AS n " + + "UNION ALL " + +"SELECT n + 1 AS n FROM t WHERE n < ?) " + // sequence generation outside of WITH + + "SELECT " + dialect.getSequenceSupport().getSelectSequenceNextValString(sequenceName) + " AS n FROM t"; + } + if (dialect instanceof org.hibernate.dialect.DB2Dialect) { + // No RECURSIVE + return "WITH t(n) AS ( " + + "SELECT 1 AS n " + // difference + + "FROM (VALUES 1) " + + "UNION ALL " + +"SELECT n + 1 AS n FROM t WHERE n < ?) " + // sequence generation outside of WITH + + "SELECT " + dialect.getSequenceSupport().getSelectSequenceNextValString(sequenceName) + " AS n FROM t"; + } + if (dialect instanceof org.hibernate.dialect.HSQLDialect) { + // https://stackoverflow.com/questions/44472280/cte-based-sequence-generation-with-hsqldb/52329862 + return "SELECT " + dialect.getSequenceSupport().getSelectSequenceNextValString(sequenceName) + " FROM UNNEST(SEQUENCE_ARRAY(1, ?, 1))"; + } + return "WITH RECURSIVE t(n, level_num) AS (" + + "SELECT " + dialect.getSequenceSupport().getSelectSequenceNextValString(sequenceName) + " AS n, 1 AS level_num " + + "UNION ALL " + + "SELECT " + dialect.getSequenceSupport().getSelectSequenceNextValString(sequenceName) + " AS n, level_num + 1 AS level_num " + + " FROM t " + + " WHERE level_num < ?) " + + "SELECT n FROM t"; + } + + private SequenceStructure buildDatabaseStructure(Type type, String sequenceName, JdbcEnvironment jdbcEnvironment) { + return new SequenceStructure(jdbcEnvironment, "orm", + QualifiedNameParser.INSTANCE.parse(sequenceName), 1, 1, type.getReturnedClass()); + } + + private static String determineSequenceName(Properties params) { + String sequenceName = params.getProperty(SEQUENCE_PARAM); + if (sequenceName == null) { + throw new MappingException("no squence name specified"); + } + return sequenceName; + } + + private static int determineFetchSize(Properties params) { + int fetchSize = ConfigurationHelper.getInt(FETCH_SIZE_PARAM, params, DEFAULT_FETCH_SIZE); + if (fetchSize <= 0) { + throw new MappingException("fetch size must be positive"); + } + return fetchSize; + } + + @Override + public boolean supportsBulkInsertionIdentifierGeneration() { + return true; + } + + @Override + public String determineBulkInsertionIdentifierGenerationSelectFragment(SqlStringGenerationContext sqlStringGenerationContext) { + return sqlStringGenerationContext.getDialect().getSequenceSupport().getSequenceNextValString(this.getSequenceName()); + } + + @Override + public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException { + this.lock.lock(); + try { + if (this.identifierPool.isEmpty()) { + this.identifierPool = this.replenishIdentifierPool(session); + } + return this.identifierPool.next(); + } finally { + this.lock.unlock(); + } + } + + private String getSequenceName() { + return this.databaseStructure.getPhysicalName().render(); + } + + @Override + public void registerExportables(Database database) { + this.databaseStructure.registerExportables(database); + } + + private IdentifierPool replenishIdentifierPool(SharedSessionContractImplementor session) + throws HibernateException { + JdbcCoordinator coordinator = session.getJdbcCoordinator(); + List identifiers = new ArrayList<>(this.fetchSize); + try (PreparedStatement statement = coordinator.getStatementPreparer().prepareStatement(this.select)) { + statement.setFetchSize(this.fetchSize); + statement.setInt(1, this.fetchSize); + try (ResultSet resultSet = coordinator.getResultSetReturn().extract(statement)) { + while (resultSet.next()) { + identifiers.add(this.identifierExtractor.extractIdentifier(resultSet)); + } + } + } catch (SQLException e) { + throw session.getJdbcServices().getSqlExceptionHelper().convert( + e, "could not get next sequence value", this.select); + } + if (identifiers.size() != this.fetchSize) { + throw new IdentifierGenerationException("expected " + this.fetchSize + " values from " + this.getSequenceName() + + " but got " + identifiers.size()); + } + return IdentifierPool.forList(identifiers); + } + + @Override + public Optimizer getOptimizer() { + return null; + } + + /** + * Holds a number of prefetched identifiers. + */ + static final class IdentifierPool { + + private final Iterator iterator; + + private IdentifierPool (List identifiers) { + this.iterator = identifiers.iterator(); + } + + static IdentifierPool forList(List identifiers) { + return new IdentifierPool(identifiers); + } + + static IdentifierPool empty() { + return new IdentifierPool(Collections.emptyList()); + } + + boolean isEmpty() { + return !this.iterator.hasNext(); + } + + Serializable next() { + return this.iterator.next(); + } + + } + + /** + * Extracts a {@link Serializable} identifier from a {@link ResultSet}. + * + * @see org.hibernate.id.IntegralDataTypeHolder + */ + enum IdentifierExtractor { + + INTEGER_IDENTIFIER_EXTRACTOR { + @Override + Serializable extractIdentifier(ResultSet resultSet) throws SQLException { + int intValue = resultSet.getInt(1); + if (resultSet.wasNull()) { + throw new IdentifierGenerationException("sequence returned null"); + } + return intValue; + } + }, + + LONG_IDENTIFIER_EXTRACTOR { + @Override + Serializable extractIdentifier(ResultSet resultSet) throws SQLException { + long longValue = resultSet.getLong(1); + if (resultSet.wasNull()) { + throw new IdentifierGenerationException("sequence returned null"); + } + return longValue; + } + }, + + BIG_INTEGER_IDENTIFIER_EXTRACTOR { + @Override + Serializable extractIdentifier(ResultSet resultSet) throws SQLException { + BigDecimal bigDecimal = resultSet.getBigDecimal(1); + if (resultSet.wasNull()) { + throw new IdentifierGenerationException("sequence returned null"); + } + return bigDecimal.setScale(0, BigDecimal.ROUND_UNNECESSARY).toBigInteger(); + } + }, + + BIG_DECIMAL_IDENTIFIER_EXTRACTOR { + @Override + Serializable extractIdentifier(ResultSet resultSet) throws SQLException { + BigDecimal bigDecimal = resultSet.getBigDecimal(1); + if (resultSet.wasNull()) { + throw new IdentifierGenerationException("sequence returned null"); + } + return bigDecimal; + } + }; + + abstract Serializable extractIdentifier(ResultSet resultSet) throws SQLException; + + static IdentifierExtractor getIdentifierExtractor(Class integralType) { + if ((integralType == Integer.class) || (integralType == int.class)) { + return INTEGER_IDENTIFIER_EXTRACTOR; + } + if ((integralType == Long.class) || (integralType == long.class)) { + return LONG_IDENTIFIER_EXTRACTOR; + } + if (integralType == BigInteger.class) { + return BIG_INTEGER_IDENTIFIER_EXTRACTOR; + } + if (integralType == BigDecimal.class) { + return BIG_DECIMAL_IDENTIFIER_EXTRACTOR; + } + throw new IdentifierGenerationException("unsupported integral type: " + integralType); + } + + } + + @Override + public String toString() { + // for debugging only + return this.getClass().getSimpleName() + '(' + this.getSequenceName() + ')'; + } + +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/naming/CamelCaseToSnakeCaseNamingStrategy.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/naming/CamelCaseToSnakeCaseNamingStrategy.java new file mode 100644 index 000000000..64b13c13d --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/naming/CamelCaseToSnakeCaseNamingStrategy.java @@ -0,0 +1,80 @@ +package com.vladmihalcea.hibernate.naming; + +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; + +/** + * Maps the JPA camelCase properties to snake_case database identifiers. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class CamelCaseToSnakeCaseNamingStrategy extends PhysicalNamingStrategyStandardImpl { + + public static final CamelCaseToSnakeCaseNamingStrategy INSTANCE = new CamelCaseToSnakeCaseNamingStrategy(); + + public static final String CAMEL_CASE_REGEX = "([a-z]+)([A-Z]+)"; + + public static final String SNAKE_CASE_PATTERN = "$1\\_$2"; + + private final Configuration configuration; + + /** + * Initialization constructor taking the default {@link Configuration} object. + */ + public CamelCaseToSnakeCaseNamingStrategy() { + this.configuration = Configuration.INSTANCE; + } + + /** + * Initialization constructor taking the {@link Class} and {@link Configuration} objects. + * + * @param configuration custom {@link Configuration} object. + */ + public CamelCaseToSnakeCaseNamingStrategy(Configuration configuration) { + this.configuration = configuration; + } + + @Override + public Identifier toPhysicalCatalogName(Identifier name, JdbcEnvironment context) { + return formatIdentifier(super.toPhysicalCatalogName(name, context)); + } + + @Override + public Identifier toPhysicalSchemaName(Identifier name, JdbcEnvironment context) { + return formatIdentifier(super.toPhysicalSchemaName(name, context)); + } + + @Override + public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) { + return formatIdentifier(super.toPhysicalTableName(name, context)); + } + + @Override + public Identifier toPhysicalSequenceName(Identifier name, JdbcEnvironment context) { + return formatIdentifier(super.toPhysicalSequenceName(name, context)); + } + + @Override + public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment context) { + return formatIdentifier(super.toPhysicalColumnName(name, context)); + } + + private Identifier formatIdentifier(Identifier identifier) { + if (identifier != null) { + String name = identifier.getText(); + + String formattedName = name.replaceAll(CAMEL_CASE_REGEX, SNAKE_CASE_PATTERN).toLowerCase(); + + return !formattedName.equals(name) ? + Identifier.toIdentifier(formattedName, identifier.isQuoted()) : + identifier; + } else { + return null; + } + + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/ListResultTransformer.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/ListResultTransformer.java new file mode 100644 index 000000000..fa2883ea6 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/ListResultTransformer.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hibernate.query; + +import org.hibernate.transform.ResultTransformer; + +import java.util.List; + +/** + * The {@link ListResultTransformer} simplifies the way + * we can use a ResultTransformer by defining a default implementation for the + * {@link ResultTransformer#transformList(List)} method. + *

+ * This way, the {@link ListResultTransformer} can be used + * as a functional interface. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + * @since 2.9.0 + */ +@FunctionalInterface +public interface ListResultTransformer extends ResultTransformer { + + /** + * Default implementation returning the tuples list as-is. + * + * @param tuples tuples list + * @return tuples list + */ + @Override + default List transformList(List tuples) { + return tuples; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/MapResultTransformer.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/MapResultTransformer.java new file mode 100644 index 000000000..8e3189021 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/MapResultTransformer.java @@ -0,0 +1,95 @@ +package com.vladmihalcea.hibernate.query; + +import org.hibernate.HibernateException; +import org.hibernate.transform.ResultTransformer; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The {@link MapResultTransformer} allows us to return + * a {@link Map} from a JPA {@link jakarta.persistence.Query}. + *

+ * If there are aliases named as {@code key} or {@code value}, + * then those will be used. + *

+ * Otherwise, the first column value is the key while the second one is the Map value. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + * @since 2.9.0 + */ +public class MapResultTransformer implements ResultTransformer { + + public static final String KEY_ALIAS = "map_key"; + + public static final String VALUE_ALIAS = "map_value"; + + private Map result = new HashMap<>(); + + /** + * Transform the tuple into a key/value pair. + *

+ * If there are aliases named as {@code key} or {@code value}, + * then those will be used. + *

+ * Otherwise, the first column value is the key while the second one is the Map value. + * + * @param tuple tuple to be transformed to a key/value pair + * @param aliases column aliases + * @return unmodified tuple + */ + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + int keyOrdinal = -1; + int valueOrdinal = -1; + + for (int i = 0; i < aliases.length; i++) { + String alias = aliases[i]; + if (KEY_ALIAS.equalsIgnoreCase(alias)) { + keyOrdinal = i; + } else if (VALUE_ALIAS.equalsIgnoreCase(alias)) { + valueOrdinal = i; + } + } + + if (keyOrdinal >= 0) { + if (valueOrdinal < 0) { + throw new HibernateException( + new IllegalArgumentException("A key column alias was given but no value column alias was found!") + ); + } + } else { + if (valueOrdinal >= 0) { + throw new HibernateException( + new IllegalArgumentException("A value column alias was given but no key column alias was found!") + ); + } else { + keyOrdinal = 0; + valueOrdinal = 1; + } + } + + @SuppressWarnings("unchecked") + K key = (K) tuple[keyOrdinal]; + @SuppressWarnings("unchecked") + V value = (V) tuple[valueOrdinal]; + result.put(key, value); + + return tuple; + } + + /** + * Return the {@link Map} instead of the default {@link List}. + * + * @param tuples tuples + * @return the {@link Map} result set + */ + @Override + public List transformList(List tuples) { + return Collections.singletonList(result); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/SQLExtractor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/SQLExtractor.java new file mode 100644 index 000000000..b4e02455e --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/SQLExtractor.java @@ -0,0 +1,29 @@ +package com.vladmihalcea.hibernate.query; + +import com.vladmihalcea.hibernate.util.ReflectionUtils; +import jakarta.persistence.Query; + +/** + * The {@link SQLExtractor} allows you to extract the + * underlying SQL query generated by a JPQL or JPA Criteria API query. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + * @since 2.9.11 + */ +public class SQLExtractor { + + protected SQLExtractor() { + } + + /** + * Get the underlying SQL generated by the provided JPA query. + * + * @param query JPA query + * @return the underlying SQL generated by the provided JPA query + */ + public static String from(Query query) { + return ReflectionUtils.invokeMethod(query, "getQueryString"); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/DynamicMutableType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/DynamicMutableType.java new file mode 100644 index 000000000..88bb26b08 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/DynamicMutableType.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hibernate.type; + +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.usertype.DynamicParameterizedType; +import org.hibernate.usertype.ParameterizedType; + +import java.sql.Types; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class DynamicMutableType> extends MutableType implements DynamicParameterizedType { + + /** + * {@inheritDoc} + */ + public DynamicMutableType(Class returnedClass, JDBC jdbcTypeDescriptor, JAVA javaTypeDescriptor) { + super(returnedClass, jdbcTypeDescriptor, javaTypeDescriptor); + } + + public DynamicMutableType(Class returnedClass, JDBC jdbcTypeDescriptor, JAVA javaTypeDescriptor, Configuration configuration) { + super(returnedClass, jdbcTypeDescriptor, javaTypeDescriptor, configuration); + } + + @Override + public void setParameterValues(Properties parameters) { + JAVA javaTypeDescriptor = getJavaTypeDescriptor(); + if(javaTypeDescriptor instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) javaTypeDescriptor; + parameterizedType.setParameterValues(parameters); + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/ImmutableType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/ImmutableType.java new file mode 100644 index 000000000..d84cb6130 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/ImmutableType.java @@ -0,0 +1,288 @@ +package com.vladmihalcea.hibernate.type; + +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.HibernateException; +import org.hibernate.MappingException; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.Mapping; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.collections.ArrayHelper; +import org.hibernate.metamodel.model.domain.BasicDomainType; +import org.hibernate.type.ForeignKeyDirection; +import org.hibernate.type.Type; +import org.hibernate.type.descriptor.java.IncomparableComparator; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.usertype.UserType; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +/** + * Very convenient base class for implementing immutable object types using Hibernate {@link UserType}. + *

+ * The {@link ImmutableType} implements the {@link Type} interface too, so you can pass all + * types extending the {@link ImmutableType} to the {@link org.hibernate.query.NativeQuery#addScalar(String, BasicDomainType)} + * method to fix the No Dialect mapping for JDBC type issues. + * + * @author Vlad Mihalcea + */ +public abstract class ImmutableType implements UserType, Type { + + private final Configuration configuration; + + private final Class clazz; + + /** + * Initialization constructor taking the {@link Class} + * and using the default {@link Configuration} object. + * + * @param clazz the entity attribute {@link Class} type to be handled + */ + protected ImmutableType(Class clazz) { + this.clazz = clazz; + this.configuration = Configuration.INSTANCE; + } + + /** + * Initialization constructor taking the {@link Class} and {@link Configuration} objects. + * + * @param clazz the entity attribute {@link Class} type to be handled + * @param configuration custom {@link Configuration} object. + */ + protected ImmutableType(Class clazz, Configuration configuration) { + this.clazz = clazz; + this.configuration = configuration; + } + + /** + * Get the current {@link Configuration} object. + * + * @return the current {@link Configuration} object. + */ + protected Configuration getConfiguration() { + return configuration; + } + + /** + * Get the column value from the JDBC {@link ResultSet}. + * + * @param rs JDBC {@link ResultSet} + * @param position database column position + * @param session current Hibernate {@link org.hibernate.Session} + * @param owner current Hibernate {@link SessionFactoryImplementor} + * @return column value + * @throws SQLException in case of failure + */ + protected abstract T get(ResultSet rs, int position, + SharedSessionContractImplementor session, Object owner) throws SQLException; + + /** + * Set the column value on the provided JDBC {@link PreparedStatement}. + * + * @param st JDBC {@link PreparedStatement} + * @param value database column value + * @param index database column index + * @param session current Hibernate {@link org.hibernate.Session} + * @throws SQLException in case of failure + */ + protected abstract void set(PreparedStatement st, T value, int index, + SharedSessionContractImplementor session) throws SQLException; + + /* Methods inherited from the {@link UserType} interface */ + + @Override + public T nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException { + return get(rs, position, session, owner); + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index, + SharedSessionContractImplementor session) throws SQLException { + set(st, clazz.cast(value), index, session); + } + + @Override + public Class returnedClass() { + return clazz; + } + + @Override + public boolean equals(Object x, Object y) { + return (x == y) || (x != null && x.equals(y)); + } + + @Override + public int hashCode(Object x) { + return x.hashCode(); + } + + @Override + public T deepCopy(Object value) { + return (T) value; + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public Serializable disassemble(Object o) { + return (Serializable) o; + } + + @Override + public T assemble(Serializable cached, Object owner) { + return (T) cached; + } + + @Override + public T replace(Object o, Object target, Object owner) { + return (T) o; + } + + /* Methods inherited from the {@link Type} interface */ + + @Override + public boolean isAssociationType() { + return false; + } + + @Override + public boolean isCollectionType() { + return false; + } + + @Override + public boolean isEntityType() { + return false; + } + + @Override + public boolean isAnyType() { + return false; + } + + @Override + public boolean isComponentType() { + return false; + } + + @Override + public int getColumnSpan(Mapping mapping) throws MappingException { + return 1; + } + + @Override + public Class getReturnedClass() { + return returnedClass(); + } + + @Override + public boolean isSame(Object x, Object y) throws HibernateException { + return equals(x, y); + } + + @Override + public boolean isEqual(Object x, Object y) throws HibernateException { + return equals(x, y); + } + + @Override + public boolean isEqual(Object x, Object y, SessionFactoryImplementor factory) throws HibernateException { + return equals(x, y); + } + + @Override + public int getHashCode(Object x) throws HibernateException { + return hashCode(x); + } + + @Override + public int getHashCode(Object x, SessionFactoryImplementor factory) throws HibernateException { + return hashCode(x); + } + + @Override + public int compare(Object x, Object y) { + return IncomparableComparator.INSTANCE.compare(x, y); + } + + @Override + public final boolean isDirty(Object old, Object current, SharedSessionContractImplementor session) { + return isDirty(old, current); + } + + @Override + public final boolean isDirty(Object old, Object current, boolean[] checkable, SharedSessionContractImplementor session) { + return checkable[0] && isDirty(old, current); + } + + protected final boolean isDirty(Object old, Object current) { + return !isSame(old, current); + } + + @Override + public boolean isModified(Object dbState, Object currentState, boolean[] checkable, SharedSessionContractImplementor session) throws HibernateException { + return isDirty(dbState, currentState); + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index, boolean[] settable, SharedSessionContractImplementor session) throws HibernateException, SQLException { + set(st, returnedClass().cast(value), index, session); + } + + @Override + public String toLoggableString(Object value, SessionFactoryImplementor factory) throws HibernateException { + return String.valueOf(value); + } + + @Override + public String getName() { + return getClass().getSimpleName(); + } + + @Override + public Object deepCopy(Object value, SessionFactoryImplementor factory) throws HibernateException { + return deepCopy(value); + } + + @Override + public Serializable disassemble(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException { + return disassemble(value); + } + + @Override + public Object assemble(Serializable cached, SharedSessionContractImplementor session, Object owner) throws HibernateException { + return assemble(cached, session); + } + + @Override + public void beforeAssemble(Serializable cached, SharedSessionContractImplementor session) { + + } + + @Override + public Object replace(Object original, Object target, SharedSessionContractImplementor session, Object owner, Map copyCache) throws HibernateException { + return replace(original, target, owner); + } + + @Override + public Object replace(Object original, Object target, SharedSessionContractImplementor session, Object owner, Map copyCache, ForeignKeyDirection foreignKeyDirection) throws HibernateException { + return replace(original, target, owner); + } + + @Override + public boolean[] toColumnNullness(Object value, Mapping mapping) { + return value == null ? ArrayHelper.FALSE : ArrayHelper.TRUE; + } + + @Override + public int[] getSqlTypeCodes(Mapping mapping) throws MappingException { + return new int[]{getSqlType()}; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/MutableType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/MutableType.java new file mode 100644 index 000000000..7ae9416fe --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/MutableType.java @@ -0,0 +1,142 @@ +package com.vladmihalcea.hibernate.type; + +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.query.BindableType; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.usertype.UserType; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Very convenient base class for implementing immutable object types using Hibernate {@link UserType}. + * + * @author Vlad Mihalcea + */ +public abstract class MutableType> implements UserType, BindableType { + + private final Class returnedClass; + + private final JDBC jdbcTypeDescriptor; + private final JAVA javaTypeDescriptor; + + private final Configuration configuration; + + /** + * Initialization constructor taking the {@link Class} + * and using the default {@link Configuration} object. + * + * @param returnedClass The class returned by {@link UserType#nullSafeGet(ResultSet, int, SharedSessionContractImplementor, Object)}. + * @param jdbcTypeDescriptor the JDBC type descriptor + * @param javaTypeDescriptor the Java type descriptor + */ + public MutableType(Class returnedClass, JDBC jdbcTypeDescriptor, JAVA javaTypeDescriptor) { + this.returnedClass = returnedClass; + this.jdbcTypeDescriptor = jdbcTypeDescriptor; + this.javaTypeDescriptor = javaTypeDescriptor; + this.configuration = Configuration.INSTANCE; + } + + /** + * Initialization constructor taking the {@link Class} + * and using the provided {@link Configuration} object. + * + * @param returnedClass The class returned by {@link UserType#nullSafeGet(ResultSet, int, SharedSessionContractImplementor, Object)}. + * @param jdbcTypeDescriptor the JDBC type descriptor + * @param javaTypeDescriptor the Java type descriptor + * @param configuration the configuration + */ + public MutableType(Class returnedClass, JDBC jdbcTypeDescriptor, JAVA javaTypeDescriptor, Configuration configuration) { + this.returnedClass = returnedClass; + this.jdbcTypeDescriptor = jdbcTypeDescriptor; + this.javaTypeDescriptor = javaTypeDescriptor; + this.configuration = configuration; + } + + public JDBC getJdbcTypeDescriptor() { + return jdbcTypeDescriptor; + } + + public JAVA getJavaTypeDescriptor() { + return javaTypeDescriptor; + } + + public Configuration getConfiguration() { + return configuration; + } + + @Override + public boolean equals(T x, T y) { + return javaTypeDescriptor.areEqual(x, y); + } + + @Override + public int hashCode(T x) { + return javaTypeDescriptor.extractHashCode(x); + } + + @Override + public T nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws + SQLException { + return jdbcTypeDescriptor.getExtractor(javaTypeDescriptor).extract(rs, position, session); + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { + jdbcTypeDescriptor.getBinder(javaTypeDescriptor).bind(st, (T) value, index, session); + } + + @Override + public T deepCopy(T value) { + return javaTypeDescriptor.getMutabilityPlan().deepCopy(value); + } + + @Override + public boolean isMutable() { + return javaTypeDescriptor.getMutabilityPlan().isMutable(); + } + + @Override + public Serializable disassemble(T value) { + return javaTypeDescriptor.getMutabilityPlan().disassemble(value, null); + } + + @Override + public T assemble(Serializable cached, Object owner) { + return javaTypeDescriptor.getMutabilityPlan().assemble(cached, null); + } + + @Override + public T replace(T detached, T managed, Object owner) { + return deepCopy(detached); + } + + @Override + public int getSqlType() { + return jdbcTypeDescriptor.getJdbcTypeCode(); + } + + @Override + public Class returnedClass() { + return returnedClass; + } + + @Override + public Class getBindableJavaType() { + return returnedClass; + } + + @Override + public SqmExpressible resolveExpressible(SessionFactoryImplementor sessionFactory) { + //@Ignore("TODO: Unsupported YET!!!") + throw new UnsupportedOperationException(); + //return sessionFactory.getTypeConfiguration().getBasicTypeRegistry().resolve( this ); + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/BooleanArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/BooleanArrayType.java new file mode 100644 index 000000000..307eb97c6 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/BooleanArrayType.java @@ -0,0 +1,51 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.BooleanArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ParameterizedParameterType; +import org.hibernate.type.spi.TypeBootstrapContext; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Properties; + +/** + * Maps a {@code boolean[]} array on a PostgreSQL ARRAY column type. + * For more details about how to use it, check out this article. + * + * @author jeet.choudhary7@gmail.com + * @version 2.9.13 + */ +public class BooleanArrayType extends AbstractArrayType { + + public static final BooleanArrayType INSTANCE = new BooleanArrayType(); + + public BooleanArrayType() { + super( + new BooleanArrayTypeDescriptor() + ); + } + + public BooleanArrayType(Configuration configuration) { + super( + new BooleanArrayTypeDescriptor(), + configuration + ); + } + + public BooleanArrayType(Class arrayClass) { + this(); + Properties parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + setParameterValues(parameters); + } + + public BooleanArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "boolean-array"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/DateArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/DateArrayType.java new file mode 100644 index 000000000..2c8260371 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/DateArrayType.java @@ -0,0 +1,49 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.DateArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ParameterizedParameterType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Date; +import java.util.Properties; + +/** + * Maps an {@code Date[]} array on a PostgreSQL date[] ARRAY type. Multidimensional arrays are supported as well, as explained in this article. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Guillaume Briand + */ +public class DateArrayType extends AbstractArrayType { + + public static final DateArrayType INSTANCE = new DateArrayType(); + + public DateArrayType() { + super( + new DateArrayTypeDescriptor() + ); + } + + public DateArrayType(Configuration configuration) { + super( + new DateArrayTypeDescriptor(), configuration + ); + } + + public DateArrayType(Class arrayClass) { + this(); + Properties parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + setParameterValues(parameters); + } + + public DateArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "date-array"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/DecimalArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/DecimalArrayType.java new file mode 100644 index 000000000..b403cc3bc --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/DecimalArrayType.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.DecimalArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ParameterizedParameterType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.math.BigDecimal; +import java.util.Properties; + +/** + * Maps a {@code decimal[]} array on a PostgreSQL ARRAY column type. + * For more details about how to use it, check out this article. + * + * @author Moritz Kobel + */ +public class DecimalArrayType extends AbstractArrayType { + + public static final DecimalArrayType INSTANCE = new DecimalArrayType(); + + public DecimalArrayType() { + super( + new DecimalArrayTypeDescriptor() + ); + } + + public DecimalArrayType(Configuration configuration) { + super( + new DecimalArrayTypeDescriptor(), + configuration + ); + } + + public DecimalArrayType(Class arrayClass) { + this(); + Properties parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + setParameterValues(parameters); + } + + public DecimalArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "decimal-array"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/DoubleArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/DoubleArrayType.java new file mode 100644 index 000000000..f44af39fe --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/DoubleArrayType.java @@ -0,0 +1,51 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.DoubleArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ParameterizedParameterType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Properties; + +/** + * Maps a {@code double[]} array on a PostgreSQL ARRAY type. Multidimensional arrays are supported as well, as explained in this article. + *

+ * For more details about how to use it, check out this + * article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class DoubleArrayType extends AbstractArrayType { + + public static final DoubleArrayType INSTANCE = new DoubleArrayType(); + + public DoubleArrayType() { + super( + new DoubleArrayTypeDescriptor() + ); + } + + public DoubleArrayType(Configuration configuration) { + super( + new DoubleArrayTypeDescriptor(), + configuration + ); + } + + public DoubleArrayType(Class arrayClass) { + this(); + Properties parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + setParameterValues(parameters); + } + + public DoubleArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "double-array"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/EnumArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/EnumArrayType.java new file mode 100644 index 000000000..326c61c51 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/EnumArrayType.java @@ -0,0 +1,70 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.array.internal.EnumArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ParameterizedParameterType; +import org.hibernate.annotations.Type; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.lang.annotation.Annotation; +import java.util.Properties; + +/** + * Maps an {@code Enum[]} array on a database ARRAY type. Multidimensional arrays are supported as well, as explained in this article. + *

+ * The {@code SQL_ARRAY_TYPE} parameter is used to define the enum type name in the database. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Nazir El-Kayssi + * @author Vlad Mihalcea + */ +public class EnumArrayType extends AbstractArrayType { + + public static final EnumArrayType INSTANCE = new EnumArrayType(); + private static final String DEFAULT_TYPE_NAME = "%s_enum_array_type"; + + private String name; + + public EnumArrayType() { + super( + new EnumArrayTypeDescriptor() + ); + } + + public EnumArrayType(Configuration configuration) { + super( + new EnumArrayTypeDescriptor(), + configuration + ); + } + + public EnumArrayType(Class arrayClass, String sqlArrayType) { + this(); + Properties parameters = new Properties(); + parameters.setProperty(SQL_ARRAY_TYPE, sqlArrayType); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + setParameterValues(parameters); + } + + public EnumArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return name; + } + + @Override + public void setParameterValues(Properties parameters) { + DynamicParameterizedType.ParameterType parameterType = (ParameterType) parameters.get(DynamicParameterizedType.PARAMETER_TYPE); + Annotation[] annotations = parameterType.getAnnotationsMethod(); + if (name == null) { + name = String.format(DEFAULT_TYPE_NAME, parameters.getProperty(SQL_ARRAY_TYPE)); + } + super.setParameterValues(parameters); + } + +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/IntArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/IntArrayType.java new file mode 100644 index 000000000..ec255319e --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/IntArrayType.java @@ -0,0 +1,48 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.IntArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ParameterizedParameterType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Properties; + +/** + * Maps an {@code int[]} array on a PostgreSQL ARRAY type. Multidimensional arrays are supported as well, as explained in this article. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class IntArrayType extends AbstractArrayType { + + public static final IntArrayType INSTANCE = new IntArrayType(); + + public IntArrayType() { + super( + new IntArrayTypeDescriptor() + ); + } + + public IntArrayType(Configuration configuration) { + super( + new IntArrayTypeDescriptor(), configuration + ); + } + + public IntArrayType(Class arrayClass) { + this(); + Properties parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + setParameterValues(parameters); + } + + public IntArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "int-array"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/ListArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/ListArrayType.java new file mode 100644 index 000000000..585dbbb99 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/ListArrayType.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.ListArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; + +/** + * Maps an {@link java.util.List} entity attribute on a PostgreSQL ARRAY column type. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class ListArrayType extends AbstractArrayType { + + public static final ListArrayType INSTANCE = new ListArrayType(); + + public ListArrayType() { + super( + new ListArrayTypeDescriptor() + ); + } + + public ListArrayType(Configuration configuration) { + super( + new ListArrayTypeDescriptor(), configuration + ); + } + + public ListArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "list-array"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/LocalDateArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/LocalDateArrayType.java new file mode 100644 index 000000000..80e9eeb28 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/LocalDateArrayType.java @@ -0,0 +1,54 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.LocalDateArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ParameterizedParameterType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Properties; + +/** + * Maps a {@code java.Time.LocalDate[]} array on a PostgreSQL date[] ARRAY type. Multidimensional arrays are + * supported as well, as + * explained in this article. + *

+ * For more details about how to use it, check out + * this + * article on vladmihalcea.com. + * + * @author Andrew Lazarus, based on DateArrayType by Guillaume Briand + */ + +public class LocalDateArrayType extends AbstractArrayType { + + public static final com.vladmihalcea.hibernate.type.array.LocalDateArrayType INSTANCE = + new com.vladmihalcea.hibernate.type.array.LocalDateArrayType(); + + public LocalDateArrayType() { + super( + new LocalDateArrayTypeDescriptor() + ); + } + + public LocalDateArrayType(Configuration configuration) { + super( + new LocalDateArrayTypeDescriptor(), configuration + ); + } + + public LocalDateArrayType(Class arrayClass) { + this(); + Properties parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + setParameterValues(parameters); + } + + public LocalDateArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "localdate-array"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/LocalDateTimeArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/LocalDateTimeArrayType.java new file mode 100644 index 000000000..624fbade0 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/LocalDateTimeArrayType.java @@ -0,0 +1,54 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.LocalDateTimeArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ParameterizedParameterType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Properties; + +/** + * Maps a {@code java.Time.LocalDateTime[]} array on a PostgreSQL timestamp[] ARRAY type. Multidimensional arrays are + * supported as well, as + * explained in this article. + *

+ * For more details about how to use it, check out + * this + * article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ + +public class LocalDateTimeArrayType extends AbstractArrayType { + + public static final LocalDateTimeArrayType INSTANCE = + new LocalDateTimeArrayType(); + + public LocalDateTimeArrayType() { + super( + new LocalDateTimeArrayTypeDescriptor() + ); + } + + public LocalDateTimeArrayType(Configuration configuration) { + super( + new LocalDateTimeArrayTypeDescriptor(), configuration + ); + } + + public LocalDateTimeArrayType(Class arrayClass) { + this(); + Properties parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + setParameterValues(parameters); + } + + public LocalDateTimeArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "localdatetime-array"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/LongArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/LongArrayType.java new file mode 100644 index 000000000..30a787e45 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/LongArrayType.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.LongArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ParameterizedParameterType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Properties; + +/** + * Maps an {@code long[]} array on a PostgreSQL ARRAY type. Multidimensional arrays are supported as well, as explained in this article. + *

+ * For more details about how to use it, check out this + * article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class LongArrayType extends AbstractArrayType { + + public static final LongArrayType INSTANCE = new LongArrayType(); + + public LongArrayType() { + super( + new LongArrayTypeDescriptor() + ); + } + + public LongArrayType(Configuration configuration) { + super( + new LongArrayTypeDescriptor(), + configuration + ); + } + + public LongArrayType(Class arrayClass) { + this(); + Properties parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + setParameterValues(parameters); + } + + public LongArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "long-array"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/StringArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/StringArrayType.java new file mode 100644 index 000000000..1ca588cfb --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/StringArrayType.java @@ -0,0 +1,49 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.StringArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ParameterizedParameterType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Properties; + +/** + * Maps an {@code String[]} array on a PostgreSQL ARRAY type. Multidimensional arrays are supported as well, as explained in this article. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class StringArrayType extends AbstractArrayType { + + public static final StringArrayType INSTANCE = new StringArrayType(); + + public StringArrayType() { + super( + new StringArrayTypeDescriptor() + ); + } + + public StringArrayType(Configuration configuration) { + super( + new StringArrayTypeDescriptor(), + configuration + ); + } + + public StringArrayType(Class arrayClass) { + this(); + Properties parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + setParameterValues(parameters); + } + + public StringArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "string-array"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/TimestampArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/TimestampArrayType.java new file mode 100644 index 000000000..b742ed2ba --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/TimestampArrayType.java @@ -0,0 +1,49 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.TimestampArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ParameterizedParameterType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Date; +import java.util.Properties; + +/** + * Maps an {@code Date[]} array on a PostgreSQL timestamp[] ARRAY type. Multidimensional arrays are supported as well, as explained in this article. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class TimestampArrayType extends AbstractArrayType { + + public static final TimestampArrayType INSTANCE = new TimestampArrayType(); + + public TimestampArrayType() { + super( + new TimestampArrayTypeDescriptor() + ); + } + + public TimestampArrayType(Configuration configuration) { + super( + new TimestampArrayTypeDescriptor(), configuration + ); + } + + public TimestampArrayType(Class arrayClass) { + this(); + Properties parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + setParameterValues(parameters); + } + + public TimestampArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "timestamp-array"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/UUIDArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/UUIDArrayType.java new file mode 100644 index 000000000..503faffd2 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/UUIDArrayType.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import com.vladmihalcea.hibernate.type.array.internal.UUIDArrayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ParameterizedParameterType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Properties; +import java.util.UUID; + +/** + * Maps an {@code UUID[]} array on a PostgreSQL ARRAY type. Multidimensional arrays are supported as well, as explained in this article. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Rafael Acevedo + */ +public class UUIDArrayType extends AbstractArrayType { + + public static final UUIDArrayType INSTANCE = new UUIDArrayType(); + + public UUIDArrayType() { + super( + new UUIDArrayTypeDescriptor() + ); + } + + public UUIDArrayType(Configuration configuration) { + super( + new UUIDArrayTypeDescriptor(), + configuration + ); + } + + public UUIDArrayType(Class arrayClass) { + this(); + Properties parameters = new Properties(); + parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass)); + setParameterValues(parameters); + } + + public UUIDArrayType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "uuid-array"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/AbstractArrayType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/AbstractArrayType.java new file mode 100644 index 000000000..c715897be --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/AbstractArrayType.java @@ -0,0 +1,32 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +import com.vladmihalcea.hibernate.type.DynamicMutableType; +import com.vladmihalcea.hibernate.type.util.Configuration; + +/** + * Base class for all ARRAY types. + * + * @author Vlad Mihalcea + */ +public abstract class AbstractArrayType + extends DynamicMutableType> { + + public static final String SQL_ARRAY_TYPE = "sql_array_type"; + + public AbstractArrayType(AbstractArrayTypeDescriptor arrayTypeDescriptor) { + super( + arrayTypeDescriptor.getJavaTypeClass(), + ArraySqlTypeDescriptor.INSTANCE, + arrayTypeDescriptor + ); + } + + public AbstractArrayType(AbstractArrayTypeDescriptor arrayTypeDescriptor, Configuration configuration) { + super( + arrayTypeDescriptor.getJavaTypeClass(), + ArraySqlTypeDescriptor.INSTANCE, + arrayTypeDescriptor, + configuration + ); + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/AbstractArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/AbstractArrayTypeDescriptor.java new file mode 100644 index 000000000..df3bf187c --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/AbstractArrayTypeDescriptor.java @@ -0,0 +1,102 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +import org.hibernate.HibernateException; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; +import org.hibernate.type.descriptor.java.MutabilityPlan; +import org.hibernate.type.descriptor.java.MutableMutabilityPlan; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.sql.Array; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Properties; + +import static com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType.SQL_ARRAY_TYPE; + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractArrayTypeDescriptor + extends AbstractClassJavaType implements DynamicParameterizedType { + + private Class arrayObjectClass; + + private String sqlArrayType; + + public AbstractArrayTypeDescriptor(Class arrayObjectClass) { + this(arrayObjectClass, (MutabilityPlan) new MutableMutabilityPlan() { + @Override + protected T deepCopyNotNull(Object value) { + return ArrayUtil.deepCopy(value); + } + }); + } + + protected AbstractArrayTypeDescriptor(Class arrayObjectClass, MutabilityPlan mutableMutabilityPlan) { + super(arrayObjectClass, mutableMutabilityPlan); + this.arrayObjectClass = arrayObjectClass; + } + + public Class getArrayObjectClass() { + return arrayObjectClass; + } + + public void setArrayObjectClass(Class arrayObjectClass) { + this.arrayObjectClass = arrayObjectClass; + } + + @Override + public void setParameterValues(Properties parameters) { + if (parameters.containsKey(PARAMETER_TYPE)) { + arrayObjectClass = (Class) ((ParameterType) parameters.get(PARAMETER_TYPE)).getReturnedClass(); + } + sqlArrayType = parameters.getProperty(SQL_ARRAY_TYPE); + } + + @Override + public boolean areEqual(Object one, Object another) { + if (one == another) { + return true; + } + if (one == null || another == null) { + return false; + } + return ArrayUtil.isEquals(one, another); + } + + @Override + public String toString(Object value) { + return Arrays.deepToString(ArrayUtil.wrapArray(value)); + } + + @Override + public String extractLoggableRepresentation(T value) { + return (value == null) ? "null" : toString(value); + } + + @SuppressWarnings({"unchecked"}) + @Override + public X unwrap(T value, Class type, WrapperOptions options) { + return (X) ArrayUtil.wrapArray(value); + } + + @Override + public T wrap(X value, WrapperOptions options) { + if (value instanceof Array) { + Array array = (Array) value; + try { + return ArrayUtil.unwrapArray((Object[]) array.getArray(), arrayObjectClass); + } catch (SQLException e) { + throw new HibernateException( + new IllegalArgumentException(e) + ); + } + } + return (T) value; + } + + protected String getSqlArrayType() { + return sqlArrayType; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/ArraySqlTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/ArraySqlTypeDescriptor.java new file mode 100644 index 000000000..873974bb4 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/ArraySqlTypeDescriptor.java @@ -0,0 +1,67 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcType; + +import java.sql.*; + +/** + * @author Vlad Mihalcea + */ +public class ArraySqlTypeDescriptor implements JdbcType { + + public static final ArraySqlTypeDescriptor INSTANCE = new ArraySqlTypeDescriptor(); + + @Override + public int getJdbcTypeCode() { + return Types.ARRAY; + } + + @Override + public ValueBinder getBinder(final JavaType JavaType) { + return new BasicBinder(JavaType, this) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + AbstractArrayTypeDescriptor abstractArrayTypeDescriptor = (AbstractArrayTypeDescriptor) JavaType; + st.setArray(index, st.getConnection().createArrayOf( + abstractArrayTypeDescriptor.getSqlArrayType(), + abstractArrayTypeDescriptor.unwrap(value, Object[].class, options) + )); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + throw new UnsupportedOperationException("Binding by name is not supported!"); + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType JavaType) { + return new BasicExtractor(JavaType, this) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return JavaType.wrap(rs.getArray(paramIndex), options); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return JavaType.wrap(statement.getArray(index), options); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return JavaType.wrap(statement.getArray(name), options); + } + + + }; + } + +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/ArrayUtil.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/ArrayUtil.java new file mode 100644 index 000000000..c4021b9e2 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/ArrayUtil.java @@ -0,0 +1,376 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +import java.lang.reflect.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * ArrayUtil - Array utilities holder. + * + * @author Vlad Mihalcea + */ +public class ArrayUtil { + + /** + * Clone an array. + * + * @param originalArray original array + * @param array element type + * @return cloned array + */ + public static T deepCopy(Object originalArray) { + Class arrayClass = originalArray.getClass(); + + if (boolean[].class.equals(arrayClass)) { + boolean[] array = (boolean[]) originalArray; + return (T) Arrays.copyOf(array, array.length); + } else if (byte[].class.equals(arrayClass)) { + byte[] array = (byte[]) originalArray; + return (T) Arrays.copyOf(array, array.length); + } else if (short[].class.equals(arrayClass)) { + short[] array = (short[]) originalArray; + return (T) Arrays.copyOf(array, array.length); + } else if (int[].class.equals(arrayClass)) { + int[] array = (int[]) originalArray; + return (T) Arrays.copyOf(array, array.length); + } else if (long[].class.equals(arrayClass)) { + long[] array = (long[]) originalArray; + return (T) Arrays.copyOf(array, array.length); + } else if (float[].class.equals(arrayClass)) { + float[] array = (float[]) originalArray; + return (T) Arrays.copyOf(array, array.length); + } else if (double[].class.equals(arrayClass)) { + double[] array = (double[]) originalArray; + return (T) Arrays.copyOf(array, array.length); + } else if (char[].class.equals(arrayClass)) { + char[] array = (char[]) originalArray; + return (T) Arrays.copyOf(array, array.length); + } else { + Object[] array = (Object[]) originalArray; + return (T) Arrays.copyOf(array, array.length); + } + } + + /** + * Wrap a given array so that primitives become wrapper objects. + * + * @param originalArray original array + * @return wrapped array + */ + public static Object[] wrapArray(Object originalArray) { + Class arrayClass = originalArray.getClass(); + + if (boolean[].class.equals(arrayClass)) { + boolean[] fromArray = (boolean[]) originalArray; + Boolean[] array = new Boolean[fromArray.length]; + for (int i = 0; i < fromArray.length; i++) { + array[i] = fromArray[i]; + } + return array; + } else if (byte[].class.equals(arrayClass)) { + byte[] fromArray = (byte[]) originalArray; + Byte[] array = new Byte[fromArray.length]; + for (int i = 0; i < fromArray.length; i++) { + array[i] = fromArray[i]; + } + return array; + } else if (short[].class.equals(arrayClass)) { + short[] fromArray = (short[]) originalArray; + Short[] array = new Short[fromArray.length]; + for (int i = 0; i < fromArray.length; i++) { + array[i] = fromArray[i]; + } + return array; + } else if (int[].class.equals(arrayClass)) { + int[] fromArray = (int[]) originalArray; + Integer[] array = new Integer[fromArray.length]; + for (int i = 0; i < fromArray.length; i++) { + array[i] = fromArray[i]; + } + return array; + } else if (long[].class.equals(arrayClass)) { + long[] fromArray = (long[]) originalArray; + Long[] array = new Long[fromArray.length]; + for (int i = 0; i < fromArray.length; i++) { + array[i] = fromArray[i]; + } + return array; + } else if (float[].class.equals(arrayClass)) { + float[] fromArray = (float[]) originalArray; + Float[] array = new Float[fromArray.length]; + for (int i = 0; i < fromArray.length; i++) { + array[i] = fromArray[i]; + } + return array; + } else if (double[].class.equals(arrayClass)) { + double[] fromArray = (double[]) originalArray; + Double[] array = new Double[fromArray.length]; + for (int i = 0; i < fromArray.length; i++) { + array[i] = fromArray[i]; + } + return array; + } else if (char[].class.equals(arrayClass)) { + char[] fromArray = (char[]) originalArray; + Character[] array = new Character[fromArray.length]; + for (int i = 0; i < fromArray.length; i++) { + array[i] = fromArray[i]; + } + return array; + } else if (originalArray instanceof List) { + return ((List) originalArray).toArray(); + } else { + return (Object[]) originalArray; + } + } + + /** + * Unwrap {@link Object[]} array to an array of the provided type + * + * @param originalArray original array + * @param arrayClass array class + * @param array element type + * @return unwrapped array + */ + public static T unwrapArray(Object[] originalArray, Class arrayClass) { + + if (boolean[].class.equals(arrayClass)) { + boolean[] array = new boolean[originalArray.length]; + for (int i = 0; i < originalArray.length; i++) { + array[i] = originalArray[i] != null ? (Boolean) originalArray[i] : Boolean.FALSE; + } + return (T) array; + } else if (byte[].class.equals(arrayClass)) { + byte[] array = new byte[originalArray.length]; + for (int i = 0; i < originalArray.length; i++) { + array[i] = originalArray[i] != null ? (Byte) originalArray[i] : 0; + } + return (T) array; + } else if (short[].class.equals(arrayClass)) { + short[] array = new short[originalArray.length]; + for (int i = 0; i < originalArray.length; i++) { + array[i] = originalArray[i] != null ? (Short) originalArray[i] : 0; + } + return (T) array; + } else if (int[].class.equals(arrayClass)) { + int[] array = new int[originalArray.length]; + for (int i = 0; i < originalArray.length; i++) { + array[i] = originalArray[i] != null ? (Integer) originalArray[i] : 0; + } + return (T) array; + } else if (long[].class.equals(arrayClass)) { + long[] array = new long[originalArray.length]; + for (int i = 0; i < originalArray.length; i++) { + array[i] = originalArray[i] != null ? (Long) originalArray[i] : 0L; + } + return (T) array; + } else if (float[].class.equals(arrayClass)) { + float[] array = new float[originalArray.length]; + for (int i = 0; i < originalArray.length; i++) { + array[i] = originalArray[i] != null ? (Float) originalArray[i] : 0f; + } + return (T) array; + } else if (double[].class.equals(arrayClass)) { + double[] array = new double[originalArray.length]; + for (int i = 0; i < originalArray.length; i++) { + array[i] = originalArray[i] != null ? (Double) originalArray[i] : 0d; + } + return (T) array; + } else if (char[].class.equals(arrayClass)) { + char[] array = new char[originalArray.length]; + for (int i = 0; i < originalArray.length; i++) { + array[i] = originalArray[i] != null ? (Character) originalArray[i] : 0; + } + return (T) array; + } else if (Enum[].class.isAssignableFrom(arrayClass)) { + T array = arrayClass.cast(Array.newInstance(arrayClass.getComponentType(), originalArray.length)); + for (int i = 0; i < originalArray.length; i++) { + Object objectValue = originalArray[i]; + if (objectValue != null) { + String stringValue = (objectValue instanceof String) ? (String) objectValue : String.valueOf(objectValue); + objectValue = Enum.valueOf((Class) arrayClass.getComponentType(), stringValue); + } + Array.set(array, i, objectValue); + } + return array; + } else if (java.time.LocalDate[].class.equals(arrayClass) && java.sql.Date[].class.equals(originalArray.getClass())) { + // special case because conversion is neither with ctor nor valueOf + Object[] array = (Object[]) Array.newInstance(java.time.LocalDate.class, originalArray.length); + for (int i = 0; i < array.length; ++i) { + array[i] = originalArray[i] != null ? ((java.sql.Date) originalArray[i]).toLocalDate() : null; + } + return (T) array; + } else if (java.time.LocalDateTime[].class.equals(arrayClass) && java.sql.Timestamp[].class.equals(originalArray.getClass())) { + // special case because conversion is neither with ctor nor valueOf + Object[] array = (Object[]) Array.newInstance(java.time.LocalDateTime.class, originalArray.length); + for (int i = 0; i < array.length; ++i) { + array[i] = originalArray[i] != null ? ((java.sql.Timestamp) originalArray[i]).toLocalDateTime() : null; + } + return (T) array; + } else if(arrayClass.getComponentType() != null && arrayClass.getComponentType().isArray()) { + int arrayLength = originalArray.length; + Object[] array = (Object[]) Array.newInstance(arrayClass.getComponentType(), arrayLength); + if (arrayLength > 0) { + for (int i = 0; i < originalArray.length; i++) { + array[i] = unwrapArray((Object[]) originalArray[i], arrayClass.getComponentType()); + } + } + return (T) array; + } else { + if (arrayClass.isInstance(originalArray)) { + return (T) originalArray; + } else { + return (T) Arrays.copyOf(originalArray, originalArray.length, (Class) arrayClass); + } + } + } + + /** + * Create array from its {@link String} representation. + * + * @param string string representation + * @param arrayClass array class + * @param array element type + * @return array + */ + public static T fromString(String string, Class arrayClass) { + String stringArray = string.replaceAll("[\\[\\]]", ""); + String[] tokens = stringArray.split(","); + + int length = tokens.length; + + if (boolean[].class.equals(arrayClass)) { + boolean[] array = new boolean[length]; + for (int i = 0; i < tokens.length; i++) { + array[i] = Boolean.valueOf(tokens[i]); + } + return (T) array; + } else if (byte[].class.equals(arrayClass)) { + byte[] array = new byte[length]; + for (int i = 0; i < tokens.length; i++) { + array[i] = Byte.valueOf(tokens[i]); + } + return (T) array; + } else if (short[].class.equals(arrayClass)) { + short[] array = new short[length]; + for (int i = 0; i < tokens.length; i++) { + array[i] = Short.valueOf(tokens[i]); + } + return (T) array; + } else if (int[].class.equals(arrayClass)) { + int[] array = new int[length]; + for (int i = 0; i < tokens.length; i++) { + array[i] = Integer.valueOf(tokens[i]); + } + return (T) array; + } else if (long[].class.equals(arrayClass)) { + long[] array = new long[length]; + for (int i = 0; i < tokens.length; i++) { + array[i] = Long.valueOf(tokens[i]); + } + return (T) array; + } else if (float[].class.equals(arrayClass)) { + float[] array = new float[length]; + for (int i = 0; i < tokens.length; i++) { + array[i] = Float.valueOf(tokens[i]); + } + return (T) array; + } else if (double[].class.equals(arrayClass)) { + double[] array = new double[length]; + for (int i = 0; i < tokens.length; i++) { + array[i] = Double.valueOf(tokens[i]); + } + return (T) array; + } else if (char[].class.equals(arrayClass)) { + char[] array = new char[length]; + for (int i = 0; i < tokens.length; i++) { + array[i] = tokens[i].length() > 0 ? tokens[i].charAt(0) : Character.MIN_VALUE; + } + return (T) array; + } else { + return (T) tokens; + } + } + + /** + * Check if two arrays are equal. + * + * @param firstArray first array + * @param secondArray second array + * @return arrays are equal + */ + public static boolean isEquals(Object firstArray, Object secondArray) { + if (firstArray.getClass() != secondArray.getClass()) { + return false; + } + Class arrayClass = firstArray.getClass(); + + if (boolean[].class.equals(arrayClass)) { + return Arrays.equals((boolean[]) firstArray, (boolean[]) secondArray); + } else if (byte[].class.equals(arrayClass)) { + return Arrays.equals((byte[]) firstArray, (byte[]) secondArray); + } else if (short[].class.equals(arrayClass)) { + return Arrays.equals((short[]) firstArray, (short[]) secondArray); + } else if (int[].class.equals(arrayClass)) { + return Arrays.equals((int[]) firstArray, (int[]) secondArray); + } else if (long[].class.equals(arrayClass)) { + return Arrays.equals((long[]) firstArray, (long[]) secondArray); + } else if (float[].class.equals(arrayClass)) { + return Arrays.equals((float[]) firstArray, (float[]) secondArray); + } else if (double[].class.equals(arrayClass)) { + return Arrays.equals((double[]) firstArray, (double[]) secondArray); + } else if (char[].class.equals(arrayClass)) { + return Arrays.equals((char[]) firstArray, (char[]) secondArray); + } else { + return Arrays.equals((Object[]) firstArray, (Object[]) secondArray); + } + } + + /** + * Get the array class for the provided array element class. + * + * @param arrayElementClass array element class + * @param array element type + * @return array class + */ + public static Class toArrayClass(Class arrayElementClass) { + + if (boolean.class.equals(arrayElementClass)) { + return (Class) boolean[].class; + } else if (byte.class.equals(arrayElementClass)) { + return (Class) byte[].class; + } else if (short.class.equals(arrayElementClass)) { + return (Class) short[].class; + } else if (int.class.equals(arrayElementClass)) { + return (Class) int[].class; + } else if (long.class.equals(arrayElementClass)) { + return (Class) long[].class; + } else if (float.class.equals(arrayElementClass)) { + return (Class) float[].class; + } else if (double[].class.equals(arrayElementClass)) { + return (Class) double[].class; + } else if (char[].class.equals(arrayElementClass)) { + return (Class) char[].class; + } else { + Object array = Array.newInstance(arrayElementClass, 0); + return (Class) array.getClass(); + } + } + + /** + * Transforms an array to a {@link List}. The reason why {@link Arrays#asList(Object[])} + * is not used is because on Java 6 it wraps the {@link List} so we end up + * with two nested {@link List} objects. + * + * @param array array to transform + * @param array element type + * @return the {@link List} representation of the array + */ + public static List asList(T[] array) { + List list = new ArrayList(array.length); + for (int i = 0; i < array.length; i++) { + list.add(i, array[i]); + } + return list; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/BooleanArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/BooleanArrayTypeDescriptor.java new file mode 100644 index 000000000..acf0fbaf8 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/BooleanArrayTypeDescriptor.java @@ -0,0 +1,18 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +/** + * @author jeet.choudhary7@gmail.com + * @version 2.9.13 + */ +public class BooleanArrayTypeDescriptor extends AbstractArrayTypeDescriptor { + + public BooleanArrayTypeDescriptor() { + super(boolean[].class); + } + + @Override + protected String getSqlArrayType() { + String sqlArrayType = super.getSqlArrayType(); + return sqlArrayType != null ? sqlArrayType : "boolean"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/DateArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/DateArrayTypeDescriptor.java new file mode 100644 index 000000000..411bf1557 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/DateArrayTypeDescriptor.java @@ -0,0 +1,18 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +import java.util.Date; + +/** + * @author Guillaume Briand + */ +public class DateArrayTypeDescriptor extends AbstractArrayTypeDescriptor { + + public DateArrayTypeDescriptor() { + super(Date[].class); + } + + @Override + protected String getSqlArrayType() { + return "date"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/DecimalArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/DecimalArrayTypeDescriptor.java new file mode 100644 index 000000000..f2d0392b5 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/DecimalArrayTypeDescriptor.java @@ -0,0 +1,18 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +import java.math.BigDecimal; + +/** + * @author Moritz Kobel + */ +public class DecimalArrayTypeDescriptor extends AbstractArrayTypeDescriptor { + + public DecimalArrayTypeDescriptor() { + super(BigDecimal[].class); + } + + @Override + protected String getSqlArrayType() { + return "decimal"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/DoubleArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/DoubleArrayTypeDescriptor.java new file mode 100644 index 000000000..e14d810ab --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/DoubleArrayTypeDescriptor.java @@ -0,0 +1,18 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +/** + * @author Vlad Mihalcea + */ +public class DoubleArrayTypeDescriptor + extends AbstractArrayTypeDescriptor { + + public DoubleArrayTypeDescriptor() { + super(double[].class); + } + + @Override + protected String getSqlArrayType() { + String sqlArrayType = super.getSqlArrayType(); + return sqlArrayType != null ? sqlArrayType : "float8"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/EnumArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/EnumArrayTypeDescriptor.java new file mode 100644 index 000000000..56777679c --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/EnumArrayTypeDescriptor.java @@ -0,0 +1,30 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +import com.vladmihalcea.hibernate.type.array.EnumArrayType; + +import java.util.Properties; + +/** + * @author Nazir El-Kayssi + * @author Vlad Mihalcea + */ +public class EnumArrayTypeDescriptor + extends AbstractArrayTypeDescriptor { + + private String sqlArrayType; + + public EnumArrayTypeDescriptor() { + super(Enum[].class); + } + + @Override + protected String getSqlArrayType() { + return sqlArrayType; + } + + @Override + public void setParameterValues(Properties parameters) { + sqlArrayType = parameters.getProperty(EnumArrayType.SQL_ARRAY_TYPE); + super.setParameterValues(parameters); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/IntArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/IntArrayTypeDescriptor.java new file mode 100644 index 000000000..92c0ea26d --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/IntArrayTypeDescriptor.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +/** + * @author Vlad Mihalcea + */ +public class IntArrayTypeDescriptor + extends AbstractArrayTypeDescriptor { + + public IntArrayTypeDescriptor() { + super(int[].class); + } + + @Override + protected String getSqlArrayType() { + return "integer"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/ListArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/ListArrayTypeDescriptor.java new file mode 100644 index 000000000..cd4e5e031 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/ListArrayTypeDescriptor.java @@ -0,0 +1,147 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +import com.vladmihalcea.hibernate.util.ReflectionUtils; +import org.hibernate.SharedSessionContract; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.MutableMutabilityPlan; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.io.Serializable; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +/** + * @author Vlad Mihalcea + */ +public class ListArrayTypeDescriptor extends AbstractArrayTypeDescriptor { + + private String sqlArrayType; + + public ListArrayTypeDescriptor() { + super(Object.class, new MutableMutabilityPlan() { + @Override + protected Object deepCopyNotNull(Object value) { + if (value instanceof List) { + Object[] array = ((List) value).toArray(); + return ArrayUtil.asList((Object[]) ArrayUtil.deepCopy(array)); + } else if (value.getClass().isArray()) { + Object[] array = (Object[]) value; + return ArrayUtil.deepCopy(array); + } else { + throw new UnsupportedOperationException("The provided " + value + " is not a List!"); + } + } + + @Override + public Object assemble(Serializable cached, SharedSessionContract session) { + if (cached != null && cached.getClass().isArray()) { + Object[] array = (Object[]) cached; + return Arrays.asList(array); + } + return super.assemble(cached, session); + } + }); + } + + @Override + protected String getSqlArrayType() { + return sqlArrayType; + } + + @Override + public Object unwrap(Object value, Class type, WrapperOptions options) { + if (value instanceof Object[]) { + return value; + } else if (value instanceof List) { + return super.unwrap(((List) value).toArray(), type, options); + } else { + throw new UnsupportedOperationException("The provided " + value + " is not a Object[] or List!"); + } + } + + @Override + public Object wrap(Object value, WrapperOptions options) { + Object wrappedObject = super.wrap(value, options); + List list = null; + if (wrappedObject != null) { + list = new ArrayList<>(); + if (wrappedObject instanceof Object[]) { + Object[] wrappedArray = (Object[]) wrappedObject; + Collections.addAll(list, wrappedArray); + } else { + throw new UnsupportedOperationException("The wrapped object " + value + " is not an Object[]!"); + } + } + return list; + } + + @Override + public boolean areEqual(Object one, Object another) { + if (one == another) { + return true; + } + if (one == null || another == null) { + return false; + } + if (one instanceof List && another instanceof List) { + return ArrayUtil.isEquals(((List) one).toArray(), ((List) another).toArray()); + } + if (one instanceof Object[] && another instanceof Object[]) { + return ArrayUtil.isEquals(one, another); + } else { + throw new UnsupportedOperationException("The provided " + one + " and " + another + " are not Object[] or List!"); + } + } + + @Override + public void setParameterValues(Properties parameters) { + Class entityClass = ReflectionUtils.getClass(parameters.getProperty(DynamicParameterizedType.ENTITY)); + String property = parameters.getProperty(DynamicParameterizedType.PROPERTY); + Type memberGenericType = ReflectionUtils.getMemberGenericTypeOrNull(entityClass, property); + if (memberGenericType instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) memberGenericType; + Type genericType = parameterizedType.getActualTypeArguments()[0]; + if (genericType instanceof WildcardType) { + genericType = ((WildcardType) genericType).getUpperBounds()[0]; + } + Class arrayElementClass = ReflectionUtils.getClass(genericType.getTypeName()); + setArrayObjectClass( + arrayElementClass.isArray() ? + arrayElementClass : + ArrayUtil.toArrayClass(arrayElementClass) + ); + sqlArrayType = parameters.getProperty(AbstractArrayType.SQL_ARRAY_TYPE); + if (sqlArrayType == null) { + if (Integer.class.isAssignableFrom(arrayElementClass)) { + sqlArrayType = "integer"; + } else if (Long.class.isAssignableFrom(arrayElementClass)) { + sqlArrayType = "bigint"; + } else if (Double.class.isAssignableFrom(arrayElementClass)) { + sqlArrayType = "float8"; + } else if (String.class.isAssignableFrom(arrayElementClass)) { + sqlArrayType = "text"; + } else if (UUID.class.isAssignableFrom(arrayElementClass)) { + sqlArrayType = "uuid"; + } else if (Date.class.isAssignableFrom(arrayElementClass) || LocalDateTime.class.isAssignableFrom(arrayElementClass)) { + sqlArrayType = "timestamp"; + } else if (Boolean.class.isAssignableFrom(arrayElementClass)) { + sqlArrayType = "boolean"; + } else if (BigDecimal.class.isAssignableFrom(arrayElementClass)) { + sqlArrayType = "decimal"; + } else if (LocalDate.class.isAssignableFrom(arrayElementClass)) { + sqlArrayType = "date"; + } else { + throw new UnsupportedOperationException("The " + arrayElementClass + " is not supported yet!"); + } + } + + } else { + throw new UnsupportedOperationException("The property " + property + " in the " + entityClass + " entity is not parameterized!"); + } + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/LocalDateArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/LocalDateArrayTypeDescriptor.java new file mode 100644 index 000000000..44d897522 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/LocalDateArrayTypeDescriptor.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +/** + * @author Vlad Mihalcea + */ +public class LocalDateArrayTypeDescriptor + extends AbstractArrayTypeDescriptor { + + public LocalDateArrayTypeDescriptor() { + super(java.time.LocalDate[].class); + } + + @Override + protected String getSqlArrayType() { + return "date"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/LocalDateTimeArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/LocalDateTimeArrayTypeDescriptor.java new file mode 100644 index 000000000..c310f3f38 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/LocalDateTimeArrayTypeDescriptor.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +/** + * @author Vlad Mihalcea + */ +public class LocalDateTimeArrayTypeDescriptor + extends AbstractArrayTypeDescriptor { + + public LocalDateTimeArrayTypeDescriptor() { + super(java.time.LocalDateTime[].class); + } + + @Override + protected String getSqlArrayType() { + return "timestamp"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/LongArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/LongArrayTypeDescriptor.java new file mode 100644 index 000000000..7228dd212 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/LongArrayTypeDescriptor.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +/** + * @author Vlad Mihalcea + */ +public class LongArrayTypeDescriptor + extends AbstractArrayTypeDescriptor { + + public LongArrayTypeDescriptor() { + super(long[].class); + } + + @Override + protected String getSqlArrayType() { + return "bigint"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/StringArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/StringArrayTypeDescriptor.java new file mode 100644 index 000000000..d4e61edf3 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/StringArrayTypeDescriptor.java @@ -0,0 +1,18 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +/** + * @author Vlad Mihalcea + */ +public class StringArrayTypeDescriptor + extends AbstractArrayTypeDescriptor { + + public StringArrayTypeDescriptor() { + super(String[].class); + } + + @Override + protected String getSqlArrayType() { + String sqlArrayType = super.getSqlArrayType(); + return sqlArrayType != null ? sqlArrayType : "text"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/TimestampArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/TimestampArrayTypeDescriptor.java new file mode 100644 index 000000000..9ec1c9840 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/TimestampArrayTypeDescriptor.java @@ -0,0 +1,18 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +public class TimestampArrayTypeDescriptor extends AbstractArrayTypeDescriptor { + + public TimestampArrayTypeDescriptor() { + super(Date[].class); + } + + @Override + protected String getSqlArrayType() { + return "timestamp"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/UUIDArrayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/UUIDArrayTypeDescriptor.java new file mode 100644 index 000000000..5fab6e631 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/array/internal/UUIDArrayTypeDescriptor.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hibernate.type.array.internal; + +import java.util.UUID; + +/** + * @author Rafael Acevedo + */ +public class UUIDArrayTypeDescriptor + extends AbstractArrayTypeDescriptor { + + public UUIDArrayTypeDescriptor() { + super(UUID[].class); + } + + @Override + protected String getSqlArrayType() { + return "uuid"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/Inet.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/Inet.java new file mode 100644 index 000000000..fa834f794 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/Inet.java @@ -0,0 +1,59 @@ +package com.vladmihalcea.hibernate.type.basic; + +import org.hibernate.HibernateException; + +import java.io.Serializable; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Objects; + +/** + * The {@link Inet} object type is used to represent an IP address. + *

+ * For more details about how to use it, + * check out this article + * on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class Inet implements Serializable { + + private final String address; + + public Inet(String address) { + this.address = address; + } + + public String getAddress() { + return address; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return Objects.equals(address, Inet.class.cast(o).address); + } + + @Override + public int hashCode() { + return Objects.hash(address); + } + + /** + * Get the associated {@link InetAddress} for the current {@link #address}. + * + * @return the associated {@link InetAddress} + */ + public InetAddress toInetAddress() { + try { + String host = address.replaceAll("\\/.*$", ""); + return Inet4Address.getByName(host); + } catch (UnknownHostException e) { + throw new HibernateException( + new IllegalArgumentException(e) + ); + } + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/Iso8601MonthType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/Iso8601MonthType.java new file mode 100644 index 000000000..b7a3c5848 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/Iso8601MonthType.java @@ -0,0 +1,45 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.MutableType; +import com.vladmihalcea.hibernate.type.basic.internal.Iso8601MonthMonthTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.type.descriptor.jdbc.IntegerJdbcType; + +import java.time.Month; + +/** + * Maps a {@link Month} object type to a {@code INT} column type + * which is saved as value from 1 (January) to 12 (December), + * according to the ISO 8601 standard. + * + * @author Martin Panzer + */ +public class Iso8601MonthType extends MutableType { + + public static final Iso8601MonthType INSTANCE = new Iso8601MonthType(); + + public Iso8601MonthType() { + super( + Month.class, + IntegerJdbcType.INSTANCE, + Iso8601MonthMonthTypeDescriptor.INSTANCE + ); + } + + public Iso8601MonthType(Configuration configuration) { + super( + Month.class, + IntegerJdbcType.INSTANCE, + Iso8601MonthMonthTypeDescriptor.INSTANCE, + configuration + ); + } + + public Iso8601MonthType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "month"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/MonthDayDateType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/MonthDayDateType.java new file mode 100644 index 000000000..38feb057c --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/MonthDayDateType.java @@ -0,0 +1,35 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.MutableType; +import com.vladmihalcea.hibernate.type.basic.internal.MonthDayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.type.descriptor.jdbc.DateJdbcType; + +import java.time.MonthDay; + +/** + * Maps a Java {@link java.time.MonthDay} object to a {@code DATE} column type. + * + * @author Mladen Savic (mladensavic94@gmail.com) + */ + +public class MonthDayDateType extends MutableType { + + public static final MonthDayDateType INSTANCE = new MonthDayDateType(); + + public MonthDayDateType() { + super(MonthDay.class, DateJdbcType.INSTANCE, MonthDayTypeDescriptor.INSTANCE); + } + + public MonthDayDateType(Configuration configuration) { + super(MonthDay.class, DateJdbcType.INSTANCE, MonthDayTypeDescriptor.INSTANCE, configuration); + } + + public MonthDayDateType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "monthday-date"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/MonthDayIntegerType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/MonthDayIntegerType.java new file mode 100644 index 000000000..13108c02d --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/MonthDayIntegerType.java @@ -0,0 +1,43 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.MutableType; +import com.vladmihalcea.hibernate.type.basic.internal.MonthDayTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.type.descriptor.jdbc.IntegerJdbcType; + +import java.time.MonthDay; + +/** + * Maps a Java {@link java.time.MonthDay} object to a {@code INT} column type. + * + * @author Mladen Savic (mladensavic94@gmail.com) + */ +public class MonthDayIntegerType extends MutableType { + + public static final MonthDayIntegerType INSTANCE = new MonthDayIntegerType(); + + public MonthDayIntegerType() { + super( + MonthDay.class, + IntegerJdbcType.INSTANCE, + MonthDayTypeDescriptor.INSTANCE + ); + } + + public MonthDayIntegerType(Configuration configuration) { + super( + MonthDay.class, + IntegerJdbcType.INSTANCE, + MonthDayTypeDescriptor.INSTANCE, + configuration + ); + } + + public MonthDayIntegerType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "monthday-int"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/NullableCharacterType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/NullableCharacterType.java new file mode 100644 index 000000000..6ddbb3d26 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/NullableCharacterType.java @@ -0,0 +1,52 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.ImmutableType; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.engine.spi.SharedSessionContractImplementor; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +/** + * Maps an {@link Character} to a nullable CHAR column type. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class NullableCharacterType extends ImmutableType { + + public static final NullableCharacterType INSTANCE = new NullableCharacterType(); + + public NullableCharacterType() { + super(Character.class); + } + + public NullableCharacterType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + super(Character.class, new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + @Override + public int getSqlType() { + return Types.CHAR; + } + + @Override + public Character get(ResultSet rs, int position, + SharedSessionContractImplementor session, Object owner) throws SQLException { + String value = rs.getString(position); + return (value != null && value.length() > 0) ? value.charAt(0) : null; + } + + @Override + public void set(PreparedStatement st, Character value, int index, + SharedSessionContractImplementor session) throws SQLException { + if (value == null) { + st.setNull(index, Types.CHAR); + } else { + st.setString(index, String.valueOf(value)); + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLCITextType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLCITextType.java new file mode 100644 index 000000000..698d44ca2 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLCITextType.java @@ -0,0 +1,45 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.ImmutableType; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.engine.spi.SharedSessionContractImplementor; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +/** + * Maps a {@link String} object type to a PostgreSQL citext + * column type. + * + * @author Sergei Portnov + */ +public class PostgreSQLCITextType extends ImmutableType { + + public static final PostgreSQLCITextType INSTANCE = new PostgreSQLCITextType(); + + public PostgreSQLCITextType() { + super(String.class); + } + + public PostgreSQLCITextType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + super(String.class, new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + @Override + public int getSqlType() { + return Types.OTHER; + } + + @Override + protected String get(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException { + Object value = rs.getObject(position); + return value == null ? null : value.toString(); + } + + @Override + protected void set(PreparedStatement st, String value, int index, SharedSessionContractImplementor session) throws SQLException { + st.setObject(index, value, Types.OTHER); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLEnumType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLEnumType.java new file mode 100644 index 000000000..a90af24be --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLEnumType.java @@ -0,0 +1,94 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.util.ReflectionUtils; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.type.descriptor.java.JavaType; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Properties; + +/** + * Maps an {@link Enum} to a PostgreSQL ENUM column type. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class PostgreSQLEnumType extends org.hibernate.type.EnumType { + + public static final PostgreSQLEnumType INSTANCE = new PostgreSQLEnumType(); + + private final Configuration configuration; + + /** + * Initialization constructor taking the default {@link Configuration} object. + */ + public PostgreSQLEnumType() { + this(Configuration.INSTANCE); + } + + /** + * Initialization constructor taking a custom {@link Configuration} object. + * + * @param configuration custom {@link Configuration} object. + */ + public PostgreSQLEnumType(Configuration configuration) { + this.configuration = configuration; + } + + public PostgreSQLEnumType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + /** + * Initialization constructor taking the {@link Class}. + * + * @param enumClass The enum type + */ + public PostgreSQLEnumType(Class enumClass) { + this(); + + Class typeConfigurationClass = ReflectionUtils.getClassOrNull("org.hibernate.type.spi.TypeConfiguration"); + + if(typeConfigurationClass != null) { + Object typeConfiguration = ReflectionUtils.newInstance(typeConfigurationClass); + + Class enumJavaTypeClass = ReflectionUtils.getClassOrNull("org.hibernate.type.descriptor.java.EnumJavaType"); + + Object enumJavaType = ReflectionUtils.newInstance(enumJavaTypeClass, new Object[] {enumClass}, new Class[]{enumClass.getClass()}); + + Object JavaTypeRegistry = ReflectionUtils.invokeGetter(typeConfiguration, "JavaTypeRegistry"); + + ReflectionUtils.invokeMethod( + JavaTypeRegistry, + ReflectionUtils.getMethod(JavaTypeRegistry, "addDescriptor", JavaType.class), + enumJavaType + ); + + ReflectionUtils.invokeSetter(this, "typeConfiguration", typeConfiguration); + } + + Properties properties = new Properties(); + properties.setProperty("enumClass", enumClass.getName()); + properties.setProperty("useNamed", Boolean.TRUE.toString()); + setParameterValues(properties); + } + + public void nullSafeSet( + PreparedStatement st, + Object value, + int index, + SharedSessionContractImplementor session) + throws HibernateException, SQLException { + st.setObject(index, value != null ? ((Enum) value).name() : null, Types.OTHER); + } + + @Override + public int getSqlType() { + return Types.VARCHAR; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLHStoreType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLHStoreType.java new file mode 100644 index 000000000..ae54cc81c --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLHStoreType.java @@ -0,0 +1,52 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.ImmutableType; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.postgresql.util.HStoreConverter; +import org.postgresql.util.PGobject; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Map; + +/** + * Maps a {@link Map} object type to a PostgreSQL hstore + * column type. + *

+ * For more details about how to use it, + * check out this article + * on vladmihalcea.com. + * + * @author Edgar Asatryan + * @author Vlad Mihalcea + */ +public class PostgreSQLHStoreType extends ImmutableType { + + public static final PostgreSQLHStoreType INSTANCE = new PostgreSQLHStoreType(); + + public PostgreSQLHStoreType() { + super(Map.class); + } + + public PostgreSQLHStoreType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + super(Map.class, new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + @Override + public int getSqlType() { + return Types.OTHER; + } + + @Override + protected Map get(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException { + return (Map) rs.getObject(position); + } + + @Override + protected void set(PreparedStatement st, Map value, int index, SharedSessionContractImplementor session) throws SQLException { + st.setObject(index, value); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLInetType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLInetType.java new file mode 100644 index 000000000..cc4816704 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLInetType.java @@ -0,0 +1,56 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.ImmutableType; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.postgresql.util.PGobject; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +/** + * Maps an {@link Inet} object type to a PostgreSQL INET column type. + *

+ * For more details about how to use it, + * check out this article + * on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class PostgreSQLInetType extends ImmutableType { + + public static final PostgreSQLInetType INSTANCE = new PostgreSQLInetType(); + + public PostgreSQLInetType() { + super(Inet.class); + } + + public PostgreSQLInetType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + super(Inet.class, new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + @Override + public int getSqlType() { + return Types.OTHER; + } + + @Override + public Inet get(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException { + String ip = rs.getString(position); + return (ip != null) ? new Inet(ip) : null; + } + + @Override + public void set(PreparedStatement st, Inet value, int index, SharedSessionContractImplementor session) throws SQLException { + if (value == null) { + st.setNull(index, Types.OTHER); + } else { + PGobject holder = new PGobject(); + holder.setType("inet"); + holder.setValue(value.getAddress()); + st.setObject(index, holder); + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearMonthDateType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearMonthDateType.java new file mode 100644 index 000000000..d739c9f56 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearMonthDateType.java @@ -0,0 +1,45 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.MutableType; +import com.vladmihalcea.hibernate.type.basic.internal.YearMonthTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.type.descriptor.jdbc.DateJdbcType; + +import java.time.YearMonth; + +/** + * Maps a Java {@link java.time.YearMonth} object to a {@code DATE} column type. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class YearMonthDateType extends MutableType { + + public static final YearMonthDateType INSTANCE = new YearMonthDateType(); + + public YearMonthDateType() { + super( + YearMonth.class, + DateJdbcType.INSTANCE, + YearMonthTypeDescriptor.INSTANCE + ); + } + + public YearMonthDateType(Configuration configuration) { + super( + YearMonth.class, + DateJdbcType.INSTANCE, + YearMonthTypeDescriptor.INSTANCE, + configuration + ); + } + + public YearMonthDateType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "yearmonth-date"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearMonthEpochType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearMonthEpochType.java new file mode 100644 index 000000000..22b4baa13 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearMonthEpochType.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.MutableType; +import com.vladmihalcea.hibernate.type.basic.internal.YearMonthEpochTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.type.descriptor.jdbc.SmallIntJdbcType; + +import java.time.YearMonth; + +/** + * Maps a Java {@link YearMonth} object to an small and continuous {@code INT} column type + * which defines the months that passed since the Unix epoch. + * + * @author Vlad Mihalcea + */ +public class YearMonthEpochType extends MutableType { + + public static final YearMonthEpochType INSTANCE = new YearMonthEpochType(); + + public YearMonthEpochType() { + super( + YearMonth.class, + SmallIntJdbcType.INSTANCE, + YearMonthEpochTypeDescriptor.INSTANCE + ); + } + + public YearMonthEpochType(Configuration configuration) { + super( + YearMonth.class, + SmallIntJdbcType.INSTANCE, + YearMonthEpochTypeDescriptor.INSTANCE, + configuration + ); + } + + public YearMonthEpochType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "yearmonth-epoch"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearMonthIntegerType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearMonthIntegerType.java new file mode 100644 index 000000000..a80055205 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearMonthIntegerType.java @@ -0,0 +1,45 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.MutableType; +import com.vladmihalcea.hibernate.type.basic.internal.YearMonthTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.type.descriptor.jdbc.IntegerJdbcType; + +import java.time.YearMonth; + +/** + * Maps a Java {@link YearMonth} object to an {@code INT} column type. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class YearMonthIntegerType extends MutableType { + + public static final YearMonthIntegerType INSTANCE = new YearMonthIntegerType(); + + public YearMonthIntegerType() { + super( + YearMonth.class, + IntegerJdbcType.INSTANCE, + YearMonthTypeDescriptor.INSTANCE + ); + } + + public YearMonthIntegerType(Configuration configuration) { + super( + YearMonth.class, + IntegerJdbcType.INSTANCE, + YearMonthTypeDescriptor.INSTANCE, + configuration + ); + } + + public YearMonthIntegerType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "yearmonth-int"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearMonthTimestampType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearMonthTimestampType.java new file mode 100644 index 000000000..d38f108ce --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearMonthTimestampType.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.MutableType; +import com.vladmihalcea.hibernate.type.basic.internal.YearMonthTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.type.descriptor.jdbc.TimestampJdbcType; + +import java.time.YearMonth; + +/** + * Maps a Java {@link YearMonth} object to a {@code TIMESTAMP} column type. + *

+ * + * @author Vlad Mihalcea + */ +public class YearMonthTimestampType extends MutableType { + + public static final YearMonthTimestampType INSTANCE = new YearMonthTimestampType(); + + public YearMonthTimestampType() { + super( + YearMonth.class, + TimestampJdbcType.INSTANCE, + YearMonthTypeDescriptor.INSTANCE + ); + } + + public YearMonthTimestampType(Configuration configuration) { + super( + YearMonth.class, + TimestampJdbcType.INSTANCE, + YearMonthTypeDescriptor.INSTANCE, + configuration + ); + } + + public YearMonthTimestampType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "yearmonth-timestamp"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearType.java new file mode 100644 index 000000000..bbd34febf --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/YearType.java @@ -0,0 +1,45 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.MutableType; +import com.vladmihalcea.hibernate.type.basic.internal.YearTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.type.descriptor.jdbc.SmallIntJdbcType; + +import java.time.Year; + +/** + * Maps a Java {@link Year} object to an {@code INT} column type. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class YearType extends MutableType { + + public static final YearType INSTANCE = new YearType(); + + public YearType() { + super( + Year.class, + SmallIntJdbcType.INSTANCE, + YearTypeDescriptor.INSTANCE + ); + } + + public YearType(Configuration configuration) { + super( + Year.class, + SmallIntJdbcType.INSTANCE, + YearTypeDescriptor.INSTANCE, + configuration + ); + } + + public YearType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "year"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/ZoneIdType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/ZoneIdType.java new file mode 100644 index 000000000..b1d9e086d --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/ZoneIdType.java @@ -0,0 +1,43 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.type.MutableType; +import com.vladmihalcea.hibernate.type.basic.internal.ZoneIdTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.type.descriptor.jdbc.VarcharJdbcType; + +import java.time.ZoneId; + +/** + * Maps a Java {@link ZoneId} object to an {@code VARCHAR} column type. + * + * @author stonio + */ +public class ZoneIdType extends MutableType { + + public static final ZoneIdType INSTANCE = new ZoneIdType(); + + public ZoneIdType() { + super( + ZoneId.class, + VarcharJdbcType.INSTANCE, + ZoneIdTypeDescriptor.INSTANCE + ); + } + + public ZoneIdType(Configuration configuration) { + super( + ZoneId.class, + VarcharJdbcType.INSTANCE, + ZoneIdTypeDescriptor.INSTANCE, + configuration + ); + } + + public ZoneIdType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public String getName() { + return "zone-id"; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/Iso8601MonthMonthTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/Iso8601MonthMonthTypeDescriptor.java new file mode 100644 index 000000000..bfd99b226 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/Iso8601MonthMonthTypeDescriptor.java @@ -0,0 +1,54 @@ +package com.vladmihalcea.hibernate.type.basic.internal; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; + +import java.time.Month; +import java.util.Objects; + +/** + * @author Martin Panzer + */ +public class Iso8601MonthMonthTypeDescriptor + extends AbstractClassJavaType { + + public static final Iso8601MonthMonthTypeDescriptor INSTANCE = new Iso8601MonthMonthTypeDescriptor(); + + public Iso8601MonthMonthTypeDescriptor() { + super(Month.class); + } + + @Override + public boolean areEqual(Month one, Month another) { + return Objects.equals(one, another); + } + + @Override + public String toString(Month value) { + return value.toString(); + } + + @SuppressWarnings({"unchecked"}) + @Override + public X unwrap(Month value, Class type, WrapperOptions options) { + if (value == null) { + return null; + } + if (Number.class.isAssignableFrom(type)) { + return (X) (Number) value.getValue(); + } + throw unknownUnwrap(type); + } + + @Override + public Month wrap(X value, WrapperOptions options) { + if (value == null) { + return null; + } + if (value instanceof Number) { + int numericValue = ((Number) (value)).intValue(); + return Month.of(numericValue); + } + throw unknownWrap(value.getClass()); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/MonthDayTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/MonthDayTypeDescriptor.java new file mode 100644 index 000000000..fe0e016bd --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/MonthDayTypeDescriptor.java @@ -0,0 +1,78 @@ +package com.vladmihalcea.hibernate.type.basic.internal; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.MonthDay; +import java.time.ZoneId; +import java.util.Date; +import java.util.Objects; + +/** + * @author Mladen Savic (mladensavic94@gmail.com) + */ + +public class MonthDayTypeDescriptor extends AbstractClassJavaType { + + public static final MonthDayTypeDescriptor INSTANCE = new MonthDayTypeDescriptor(); + + protected MonthDayTypeDescriptor() { + super(MonthDay.class); + } + + @SuppressWarnings({"unchecked"}) + @Override + public X unwrap(MonthDay monthDay, Class type, WrapperOptions wrapperOptions) { + if (monthDay == null) { + return null; + } + if (String.class.isAssignableFrom(type)) { + return (X) toString(monthDay); + } + if (Number.class.isAssignableFrom(type)) { + Integer numericValue = (monthDay.getMonthValue() * 100) + monthDay.getDayOfMonth(); + return (X) (numericValue); + } + if (Date.class.isAssignableFrom(type)) { + int currentYear = LocalDate.now().getYear(); + return (X) java.sql.Date.valueOf(monthDay.atYear(currentYear)); + } + + throw unknownUnwrap(type); + } + + @Override + public MonthDay wrap(X value, WrapperOptions wrapperOptions) { + if (value == null) { + return null; + } + if (value instanceof String) { + return fromString((String) value); + } + if (value instanceof Number) { + int numericValue = ((Number) (value)).intValue(); + int month = numericValue / 100; + int dayOfMonth = numericValue % 100; + return MonthDay.of(month, dayOfMonth); + } + if (value instanceof Date) { + Date date = (Date) value; + return MonthDay.from(Instant.ofEpochMilli(date.getTime()) + .atZone(ZoneId.systemDefault()) + .toLocalDate()); + } + throw unknownWrap(value.getClass()); + } + + @Override + public boolean areEqual(MonthDay one, MonthDay another) { + return Objects.equals(one, another); + } + + @Override + public String toString(MonthDay value) { + return value.toString(); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/YearMonthEpochTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/YearMonthEpochTypeDescriptor.java new file mode 100644 index 000000000..a288c666d --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/YearMonthEpochTypeDescriptor.java @@ -0,0 +1,70 @@ +package com.vladmihalcea.hibernate.type.basic.internal; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; + +import java.math.BigInteger; +import java.time.LocalDate; +import java.time.Period; +import java.time.YearMonth; +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +public class YearMonthEpochTypeDescriptor + extends AbstractClassJavaType { + + public static final YearMonth YEAR_MONTH_EPOCH = YearMonth.of(1970, 1); + + public static final LocalDate LOCAL_DATE_EPOCH = YEAR_MONTH_EPOCH.atDay(1); + + public static final YearMonthEpochTypeDescriptor INSTANCE = new YearMonthEpochTypeDescriptor(); + + public YearMonthEpochTypeDescriptor() { + super(YearMonth.class); + } + + @Override + public boolean areEqual(YearMonth one, YearMonth another) { + return Objects.equals(one, another); + } + + @Override + public String toString(YearMonth value) { + return value.toString(); + } + + @SuppressWarnings({"unchecked"}) + @Override + public X unwrap(YearMonth value, Class type, WrapperOptions options) { + if (value == null) { + return null; + } + Number monthsSinceEpoch = Period.between(LOCAL_DATE_EPOCH, value.atDay(1)).toTotalMonths(); + if (Short.class.isAssignableFrom(type)) { + return (X) (Short) (monthsSinceEpoch.shortValue()); + } + if (Integer.class.isAssignableFrom(type)) { + return (X) (Integer) (monthsSinceEpoch.intValue()); + } + if (Long.class.isAssignableFrom(type)) { + return (X) (Long) (monthsSinceEpoch.longValue()); + } + if (BigInteger.class.isAssignableFrom(type)) { + return (X) (BigInteger.valueOf(monthsSinceEpoch.longValue())); + } + throw unknownUnwrap(type); + } + + @Override + public YearMonth wrap(X value, WrapperOptions options) { + if (value == null) { + return null; + } + if (value instanceof Number) { + return YEAR_MONTH_EPOCH.plusMonths(((Number) value).intValue()); + } + throw unknownWrap(value.getClass()); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/YearMonthTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/YearMonthTypeDescriptor.java new file mode 100644 index 000000000..028b7e586 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/YearMonthTypeDescriptor.java @@ -0,0 +1,83 @@ +package com.vladmihalcea.hibernate.type.basic.internal; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.YearMonth; +import java.time.ZoneId; +import java.util.Date; +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +public class YearMonthTypeDescriptor + extends AbstractClassJavaType { + + public static final YearMonthTypeDescriptor INSTANCE = new YearMonthTypeDescriptor(); + + public YearMonthTypeDescriptor() { + super(YearMonth.class); + } + + @Override + public boolean areEqual(YearMonth one, YearMonth another) { + return Objects.equals(one, another); + } + + @Override + public String toString(YearMonth value) { + return value.toString(); + } + + @SuppressWarnings({"unchecked"}) + @Override + public X unwrap(YearMonth value, Class type, WrapperOptions options) { + if (value == null) { + return null; + } + if (String.class.isAssignableFrom(type)) { + return (X) toString(value); + } + if (Number.class.isAssignableFrom(type)) { + Integer numericValue = (value.getYear() * 100) + value.getMonth().getValue(); + return (X) (numericValue); + } + if (Timestamp.class.isAssignableFrom(type)) { + return (X) java.sql.Timestamp.valueOf(value.atDay(1).atStartOfDay()); + } + if (Date.class.isAssignableFrom(type)) { + return (X) java.sql.Date.valueOf(value.atDay(1)); + } + throw unknownUnwrap(type); + } + + @Override + public YearMonth wrap(X value, WrapperOptions options) { + if (value == null) { + return null; + } + if (value instanceof String) { + return fromString((String) value); + } + if (value instanceof Number) { + int numericValue = ((Number) (value)).intValue(); + if(numericValue > 0) { + int year = numericValue / 100; + int month = numericValue % 100; + return YearMonth.of(year, month); + } else { + return null; + } + } + if (value instanceof Date) { + Date date = (Date) value; + return YearMonth.from(Instant.ofEpochMilli(date.getTime()) + .atZone(ZoneId.systemDefault()) + .toLocalDate()); + } + throw unknownWrap(value.getClass()); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/YearTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/YearTypeDescriptor.java new file mode 100644 index 000000000..63bf2bb32 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/YearTypeDescriptor.java @@ -0,0 +1,61 @@ +package com.vladmihalcea.hibernate.type.basic.internal; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; + +import java.time.Year; +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +public class YearTypeDescriptor + extends AbstractClassJavaType { + + public static final YearTypeDescriptor INSTANCE = new YearTypeDescriptor(); + + public YearTypeDescriptor() { + super(Year.class); + } + + @Override + public boolean areEqual(Year one, Year another) { + return Objects.equals(one, another); + } + + @Override + public String toString(Year value) { + return value.toString(); + } + + @SuppressWarnings({"unchecked"}) + @Override + public X unwrap(Year value, Class type, WrapperOptions options) { + if (value == null) { + return null; + } + if (String.class.isAssignableFrom(type)) { + return (X) toString(value); + } + if (Number.class.isAssignableFrom(type)) { + Short numericValue = (short) value.getValue(); + return (X) (numericValue); + } + throw unknownUnwrap(type); + } + + @Override + public Year wrap(X value, WrapperOptions options) { + if (value == null) { + return null; + } + if (value instanceof String) { + return fromString((String) value); + } + if (value instanceof Number) { + short numericValue = ((Number) (value)).shortValue(); + return Year.of(numericValue); + } + throw unknownWrap(value.getClass()); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/ZoneIdTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/ZoneIdTypeDescriptor.java new file mode 100644 index 000000000..d3e5c4a55 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/basic/internal/ZoneIdTypeDescriptor.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hibernate.type.basic.internal; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; + +import java.time.ZoneId; +import java.util.Comparator; + +/** + * Descriptor for {@link ZoneId} handling. + * + * @author stonio + */ +public class ZoneIdTypeDescriptor extends AbstractClassJavaType { + + public static final ZoneIdTypeDescriptor INSTANCE = new ZoneIdTypeDescriptor(); + + public static class ZoneIdComparator implements Comparator { + public static final ZoneIdComparator INSTANCE = new ZoneIdComparator(); + + public int compare(ZoneId o1, ZoneId o2) { + return o1.getId().compareTo(o2.getId()); + } + } + + public ZoneIdTypeDescriptor() { + super(ZoneId.class); + } + + public String toString(ZoneId value) { + return value.getId(); + } + + public ZoneId fromString(String string) { + return ZoneId.of(string); + } + + @Override + public Comparator getComparator() { + return ZoneIdComparator.INSTANCE; + } + + @SuppressWarnings({ "unchecked" }) + public X unwrap(ZoneId value, Class type, WrapperOptions options) { + if (value == null) { + return null; + } + if (String.class.isAssignableFrom(type)) { + return (X) toString(value); + } + throw unknownUnwrap(type); + } + + public ZoneId wrap(X value, WrapperOptions options) { + if (value == null) { + return null; + } + if (String.class.isInstance(value)) { + return fromString((String) value); + } + throw unknownWrap(value.getClass()); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/interval/OracleIntervalDayToSecondType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/interval/OracleIntervalDayToSecondType.java new file mode 100644 index 000000000..98c887d72 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/interval/OracleIntervalDayToSecondType.java @@ -0,0 +1,90 @@ +package com.vladmihalcea.hibernate.type.interval; + +import com.vladmihalcea.hibernate.type.ImmutableType; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Maps a Java {@link Duration} object to a Oracle IntervalDS column type. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + * @since 2.6.1 + */ +public class OracleIntervalDayToSecondType extends ImmutableType { + + public static final OracleIntervalDayToSecondType INSTANCE = new OracleIntervalDayToSecondType(); + + public OracleIntervalDayToSecondType() { + super(Duration.class); + } + + public OracleIntervalDayToSecondType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + super(Duration.class, new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + private static final int SQL_COLUMN_TYPE = -104; + private static final String INTERVAL_TOKENS = "%1$2d %2$2d:%3$2d:%4$2d.0"; + private static final Pattern INTERVAL_PATTERN = Pattern.compile("(\\d+)\\s(\\d+):(\\d+):(\\d+)\\.\\d+"); + + @Override + protected Duration get( + ResultSet rs, + int position, + SharedSessionContractImplementor session, + Object owner) throws SQLException { + final String intervalValue = rs.getString(position); + + if (intervalValue == null) { + return null; + } + + Matcher matcher = INTERVAL_PATTERN.matcher(intervalValue); + + if (matcher.matches()) { + Integer days = Integer.parseInt(matcher.group(1)); + Integer hours = Integer.parseInt(matcher.group(2)); + Integer minutes = Integer.parseInt(matcher.group(3)); + Integer seconds = Integer.parseInt(matcher.group(4)); + + return Duration.ofDays(days) + .plus(hours, ChronoUnit.HOURS) + .plus(minutes, ChronoUnit.MINUTES) + .plus((long) Math.floor(seconds), ChronoUnit.SECONDS); + } + + throw new HibernateException( + new IllegalArgumentException("The parsed interval " + intervalValue + " does not match the expected pattern: " + INTERVAL_PATTERN) + ); + } + + @Override + protected void set(PreparedStatement st, Duration value, int index, SharedSessionContractImplementor session) throws SQLException { + if (value == null) { + st.setNull(index, SQL_COLUMN_TYPE); + } else { + final int days = (int) value.toDays(); + final int hours = (int) (value.toHours() % 24); + final int minutes = (int) (value.toMinutes() % 60); + final int seconds = (int) (value.getSeconds() % 60); + + st.setString(index, String.format(INTERVAL_TOKENS, days, hours, minutes, seconds)); + } + } + + @Override + public int getSqlType() { + return SQL_COLUMN_TYPE; + } + +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/interval/PostgreSQLIntervalType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/interval/PostgreSQLIntervalType.java new file mode 100644 index 000000000..4f46d7cc9 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/interval/PostgreSQLIntervalType.java @@ -0,0 +1,79 @@ +package com.vladmihalcea.hibernate.type.interval; + +import com.vladmihalcea.hibernate.type.ImmutableType; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.postgresql.util.PGInterval; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +/** + * Maps a Java {@link Duration} object to a PostgreSQL Interval column type. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Jan-Willem Gmelig Meyling + * @author Vlad Mihalcea + * @since 2.5.1 + */ +public class PostgreSQLIntervalType extends ImmutableType { + + private static final double MICROS_IN_SECOND = 1000000; + + public static final PostgreSQLIntervalType INSTANCE = new PostgreSQLIntervalType(); + + public PostgreSQLIntervalType() { + super(Duration.class); + } + + public PostgreSQLIntervalType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + super(Duration.class, new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + @Override + protected Duration get(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException { + final PGInterval interval = (PGInterval) rs.getObject(position); + + if (interval == null) { + return null; + } + + final int days = interval.getDays(); + final int hours = interval.getHours(); + final int minutes = interval.getMinutes(); + final int seconds = (int) interval.getSeconds(); + final int micros = (int) Math.round((interval.getSeconds() - seconds) * MICROS_IN_SECOND); + + return Duration.ofDays(days) + .plus(hours, ChronoUnit.HOURS) + .plus(minutes, ChronoUnit.MINUTES) + .plus(seconds, ChronoUnit.SECONDS) + .plus(micros, ChronoUnit.MICROS); + } + + @Override + protected void set(PreparedStatement st, Duration value, int index, SharedSessionContractImplementor session) throws SQLException { + if (value == null) { + st.setNull(index, Types.OTHER); + } else { + final int days = (int) value.toDays(); + final int hours = (int) (value.toHours() % 24); + final int minutes = (int) (value.toMinutes() % 60); + final int seconds = (int) (value.getSeconds() % 60); + final int micros = value.getNano() / 1000; + final double secondsWithFraction = seconds + (micros / MICROS_IN_SECOND); + st.setObject(index, new PGInterval(0, 0, days, hours, minutes, secondsWithFraction)); + } + } + + @Override + public int getSqlType() { + return Types.OTHER; + } + +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/interval/PostgreSQLPeriodType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/interval/PostgreSQLPeriodType.java new file mode 100644 index 000000000..9bb9f4d14 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/interval/PostgreSQLPeriodType.java @@ -0,0 +1,68 @@ +package com.vladmihalcea.hibernate.type.interval; + +import com.vladmihalcea.hibernate.type.ImmutableType; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.postgresql.util.PGInterval; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.Duration; +import java.time.Period; + +/** + * Maps a Java {@link Duration} object to a PostgreSQL Interval column type. + * + * @author Jan-Willem Gmelig Meyling + * @author Vlad Mihalcea + * @since 2.6.2 + */ +public class PostgreSQLPeriodType extends ImmutableType { + + public static final PostgreSQLPeriodType INSTANCE = new PostgreSQLPeriodType(); + + public PostgreSQLPeriodType() { + super(Period.class); + } + + public PostgreSQLPeriodType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + super(Period.class, new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + @Override + protected Period get(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException { + final PGInterval interval = (PGInterval) rs.getObject(position); + + if (interval == null) { + return null; + } + + final int years = interval.getYears(); + final int months = interval.getMonths(); + final int days = interval.getDays(); + + return Period.ofYears(years) + .plusMonths(months) + .plusDays(days); + } + + @Override + protected void set(PreparedStatement st, Period value, int index, SharedSessionContractImplementor session) throws SQLException { + if (value == null) { + st.setNull(index, Types.OTHER); + } else { + final int days = value.getDays(); + final int months = value.getMonths(); + final int years = value.getYears(); + st.setObject(index, new PGInterval(years, months, days, 0, 0, 0)); + } + } + + @Override + public int getSqlType() { + return Types.OTHER; + } + +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBinaryType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBinaryType.java new file mode 100644 index 000000000..325885f55 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBinaryType.java @@ -0,0 +1,95 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vladmihalcea.hibernate.type.DynamicMutableType; +import com.vladmihalcea.hibernate.type.json.internal.JsonBinaryJdbcTypeDescriptor; +import com.vladmihalcea.hibernate.type.json.internal.JsonJavaTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; + +import java.lang.reflect.Type; + +/** + *

+ * Maps any given Java object on a JSON column type that is managed via {@link java.sql.PreparedStatement#setObject(int, Object)} at JDBC Driver level. + *

+ *

+ * If you are using PostgreSQL, you can use this {@link JsonBinaryType} to map both jsonb and json column types. + *

+ *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + *

+ *

+ * If you want to use a more portable Hibernate Type that can work on Oracle, SQL Server, PostgreSQL, MySQL, or H2 without any configuration changes, then you should use the {@link JsonType} instead. + *

+ * + * @author Vlad Mihalcea + */ +public class JsonBinaryType extends DynamicMutableType { + + public static final JsonBinaryType INSTANCE = new JsonBinaryType(); + + public JsonBinaryType() { + super( + Object.class, + JsonBinaryJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper()) + ); + } + + public JsonBinaryType(Type javaType) { + super( + Object.class, + JsonBinaryJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper(), javaType) + ); + } + + public JsonBinaryType(Configuration configuration) { + super( + Object.class, + JsonBinaryJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(configuration.getObjectMapperWrapper()) + ); + } + + public JsonBinaryType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public JsonBinaryType(ObjectMapper objectMapper) { + super( + Object.class, + JsonBinaryJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(new ObjectMapperWrapper(objectMapper)) + ); + } + + public JsonBinaryType(ObjectMapperWrapper objectMapperWrapper) { + super( + Object.class, + JsonBinaryJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(objectMapperWrapper) + ); + } + + public JsonBinaryType(ObjectMapper objectMapper, Type javaType) { + super( + Object.class, + JsonBinaryJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(new ObjectMapperWrapper(objectMapper), javaType) + ); + } + + public JsonBinaryType(ObjectMapperWrapper objectMapperWrapper, Type javaType) { + super( + Object.class, + JsonBinaryJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(objectMapperWrapper, javaType) + ); + } + + public String getName() { + return "jsonb"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java new file mode 100644 index 000000000..c47ffaa71 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonBlobType.java @@ -0,0 +1,96 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vladmihalcea.hibernate.type.DynamicMutableType; +import com.vladmihalcea.hibernate.type.json.internal.JsonJavaTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; +import org.hibernate.type.descriptor.jdbc.BlobJdbcType; + +import java.lang.reflect.Type; +import java.sql.Blob; + +/** + *

+ * Maps any given Java object on a JSON column type that is managed via {@link java.sql.PreparedStatement#setBlob(int, Blob)} at JDBC Driver level. + *

+ *

+ * If you are using Oracle, you can use this {@link JsonBlobType} to map a {@code BLOB} column type storing JSON. + *

+ *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + *

+ *

+ * If you want to use a more portable Hibernate Type that can work on Oracle, SQL Server, PostgreSQL, MySQL, or H2 without any configuration changes, then you should use the {@link JsonType} instead. + *

+ * + * @author Vlad Mihalcea + */ +public class JsonBlobType extends DynamicMutableType { + + public static final JsonBlobType INSTANCE = new JsonBlobType(); + + public JsonBlobType() { + super( + Object.class, + org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + new JsonJavaTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper()) + ); + } + + public JsonBlobType(Type javaType) { + super( + Object.class, + org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + new JsonJavaTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper(), javaType) + ); + } + + public JsonBlobType(Configuration configuration) { + super( + Object.class, + org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + new JsonJavaTypeDescriptor(configuration.getObjectMapperWrapper()) + ); + } + + public JsonBlobType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public JsonBlobType(ObjectMapper objectMapper) { + super( + Object.class, + org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + new JsonJavaTypeDescriptor(new ObjectMapperWrapper(objectMapper)) + ); + } + + public JsonBlobType(ObjectMapperWrapper objectMapperWrapper) { + super( + Object.class, + org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + new JsonJavaTypeDescriptor(objectMapperWrapper) + ); + } + + public JsonBlobType(ObjectMapper objectMapper, Type javaType) { + super( + Object.class, + org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + new JsonJavaTypeDescriptor(new ObjectMapperWrapper(objectMapper), javaType) + ); + } + + public JsonBlobType(ObjectMapperWrapper objectMapperWrapper, Type javaType) { + super( + Object.class, + org.hibernate.type.descriptor.jdbc.BlobJdbcType.DEFAULT, + new JsonJavaTypeDescriptor(objectMapperWrapper, javaType) + ); + } + + public String getName() { + return "jsonb-lob"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonNodeBinaryType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonNodeBinaryType.java new file mode 100644 index 000000000..939126958 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonNodeBinaryType.java @@ -0,0 +1,70 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vladmihalcea.hibernate.type.DynamicMutableType; +import com.vladmihalcea.hibernate.type.json.internal.JsonBinaryJdbcTypeDescriptor; +import com.vladmihalcea.hibernate.type.json.internal.JsonNodeJavaTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; + +/** + *

+ * Maps a Jackson {@link JsonNode} object on a JSON column type that is managed via {@link java.sql.PreparedStatement#setObject(int, Object)} at JDBC Driver level. + *

+ *

+ * For instance, if you are using PostgreSQL, you can use the {@link JsonNodeBinaryType} to map both {@code jsonb} and {@code json} column types to a Jackson {@link JsonNode} object. + *

+ *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + *

+ *

+ * If you want to use a more portable Hibernate Type that can work on Oracle, SQL Server, PostgreSQL, MySQL, or H2 without any configuration changes, then you should use the {@link JsonType} instead. + *

+ * + * @author Vlad Mihalcea + */ +public class JsonNodeBinaryType extends DynamicMutableType { + + public static final JsonNodeBinaryType INSTANCE = new JsonNodeBinaryType(); + + public JsonNodeBinaryType() { + super( + JsonNode.class, + JsonBinaryJdbcTypeDescriptor.INSTANCE, + new JsonNodeJavaTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper()) + ); + } + + public JsonNodeBinaryType(Configuration configuration) { + super( + JsonNode.class, + JsonBinaryJdbcTypeDescriptor.INSTANCE, + new JsonNodeJavaTypeDescriptor(configuration.getObjectMapperWrapper()) + ); + } + + public JsonNodeBinaryType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public JsonNodeBinaryType(ObjectMapper objectMapper) { + super( + JsonNode.class, + JsonBinaryJdbcTypeDescriptor.INSTANCE, + new JsonNodeJavaTypeDescriptor(new ObjectMapperWrapper(objectMapper)) + ); + } + + public JsonNodeBinaryType(ObjectMapperWrapper objectMapperWrapper) { + super( + JsonNode.class, + JsonBinaryJdbcTypeDescriptor.INSTANCE, + new JsonNodeJavaTypeDescriptor(objectMapperWrapper) + ); + } + + public String getName() { + return "jsonb-node"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonNodeStringType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonNodeStringType.java new file mode 100644 index 000000000..aea933eaa --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonNodeStringType.java @@ -0,0 +1,70 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vladmihalcea.hibernate.type.DynamicMutableType; +import com.vladmihalcea.hibernate.type.json.internal.JsonNodeJavaTypeDescriptor; +import com.vladmihalcea.hibernate.type.json.internal.JsonStringJdbcTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; + +/** + *

+ * Maps a Jackson {@link JsonNode} object on a JSON column type that is managed via {@link java.sql.PreparedStatement#setString(int, String)} at JDBC Driver level. + *

+ *

+ * For instance, if you are using MySQL, you can use the {@link JsonNodeStringType} to map the {@code json} column type to a Jackson {@link JsonNode} object. + *

+ *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + *

+ *

+ * If you want to use a more portable Hibernate Type that can work on Oracle, SQL Server, PostgreSQL, MySQL, or H2 without any configuration changes, then you should use the {@link JsonType} instead. + *

+ * + * @author Vlad Mihalcea + */ +public class JsonNodeStringType extends DynamicMutableType { + + public static final JsonNodeStringType INSTANCE = new JsonNodeStringType(); + + public JsonNodeStringType() { + super( + JsonNode.class, + JsonStringJdbcTypeDescriptor.INSTANCE, + new JsonNodeJavaTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper()) + ); + } + + public JsonNodeStringType(Configuration configuration) { + super( + JsonNode.class, + JsonStringJdbcTypeDescriptor.INSTANCE, + new JsonNodeJavaTypeDescriptor(configuration.getObjectMapperWrapper()) + ); + } + + public JsonNodeStringType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public JsonNodeStringType(ObjectMapper objectMapper) { + super( + JsonNode.class, + JsonStringJdbcTypeDescriptor.INSTANCE, + new JsonNodeJavaTypeDescriptor(new ObjectMapperWrapper(objectMapper)) + ); + } + + public JsonNodeStringType(ObjectMapperWrapper objectMapperWrapper) { + super( + JsonNode.class, + JsonStringJdbcTypeDescriptor.INSTANCE, + new JsonNodeJavaTypeDescriptor(objectMapperWrapper) + ); + } + + public String getName() { + return "jsonb-node"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonStringType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonStringType.java new file mode 100644 index 000000000..130f748ba --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonStringType.java @@ -0,0 +1,102 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vladmihalcea.hibernate.type.DynamicMutableType; +import com.vladmihalcea.hibernate.type.json.internal.JsonJavaTypeDescriptor; +import com.vladmihalcea.hibernate.type.json.internal.JsonStringJdbcTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; + +import java.lang.reflect.Type; + +/** + *

+ * Maps any given Java object on a JSON column type that is managed via {@link java.sql.PreparedStatement#setString(int, String)} at JDBC Driver level. + *

+ *
    + *
  • If you are using Oracle, you can use this {@link JsonStringType} to map a {@code VARCHAR2} column type storing JSON. For more details, check out this article on vladmihalcea.com. + *
  • + *
  • + * If you are using SQL Server, you can use this {@link JsonStringType} to map an {@code NVARCHAR} column type storing JSON. For more details, check out this article on vladmihalcea.com. + *
  • + *
  • + * If you are using MySQL, you can use this {@link JsonStringType} to map the {@code json} column type. For more details, check out this article on vladmihalcea.com. + *
  • + *
  • + * If you are using PostgreSQL, then you should NOT use this {@link JsonStringType}. You should use {@link JsonBinaryType} instead. For more details, check out this article on vladmihalcea.com. + *
  • + *
+ *

+ * If you want to use a more portable Hibernate Type that can work on Oracle, SQL Server, PostgreSQL, MySQL, or H2 without any configuration changes, then you should use the {@link JsonType} instead. + *

+ * + * @author Vlad Mihalcea + */ +public class JsonStringType extends DynamicMutableType { + + public static final JsonStringType INSTANCE = new JsonStringType(); + + public JsonStringType() { + super( + Object.class, + JsonStringJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper()) + ); + } + + public JsonStringType(Type javaType) { + super( + Object.class, + JsonStringJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper(), javaType) + ); + } + + public JsonStringType(Configuration configuration) { + super( + Object.class, + JsonStringJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(configuration.getObjectMapperWrapper()) + ); + } + + public JsonStringType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public JsonStringType(ObjectMapper objectMapper) { + super( + Object.class, + JsonStringJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(new ObjectMapperWrapper(objectMapper)) + ); + } + + public JsonStringType(ObjectMapperWrapper objectMapperWrapper) { + super( + Object.class, + JsonStringJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(objectMapperWrapper) + ); + } + + public JsonStringType(ObjectMapper objectMapper, Type javaType) { + super( + Object.class, + JsonStringJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(new ObjectMapperWrapper(objectMapper), javaType) + ); + } + + public JsonStringType(ObjectMapperWrapper objectMapperWrapper, Type javaType) { + super( + Object.class, + JsonStringJdbcTypeDescriptor.INSTANCE, + new JsonJavaTypeDescriptor(objectMapperWrapper, javaType) + ); + } + + public String getName() { + return "json"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java new file mode 100644 index 000000000..7453c7ea7 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/JsonType.java @@ -0,0 +1,100 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vladmihalcea.hibernate.type.DynamicMutableType; +import com.vladmihalcea.hibernate.type.json.internal.JsonJavaTypeDescriptor; +import com.vladmihalcea.hibernate.type.json.internal.JsonJdbcTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; + +import java.lang.reflect.Type; + +/** + *

+ * {@link JsonType} allows you to map any given JSON object (e.g., POJO, Map<String, Object>, List<T>, JsonNode) on any of the following database systems: + *

+ *
    + *
  • PostgreSQL - for both jsonb and json column types
  • + *
  • MySQL - for the json column type
  • + *
  • SQL Server - for the NVARCHAR column type storing JSON
  • + *
  • Oracle - for the VARCHAR column type storing JSON
  • + *
  • H2 - for the json column type
  • + *
+ * + *

+ * For more details about how to use the {@link JsonType}, check out this article on vladmihalcea.com. + *

+ *

+ * If you are using Oracle and want to store JSON objects in a BLOB column types, then you should use the {@link JsonBlobType} instead. For more details, check out this article on vladmihalcea.com. + *

+ * + * @author Vlad Mihalcea + */ +public class JsonType extends DynamicMutableType { + + public static final JsonType INSTANCE = new JsonType(); + + public JsonType() { + super( + Object.class, + new JsonJdbcTypeDescriptor(), + new JsonJavaTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper()) + ); + } + + public JsonType(Type javaType) { + super( + Object.class, + new JsonJdbcTypeDescriptor(), + new JsonJavaTypeDescriptor(Configuration.INSTANCE.getObjectMapperWrapper(), javaType) + ); + } + + public JsonType(Configuration configuration) { + super( + Object.class, + new JsonJdbcTypeDescriptor(), + new JsonJavaTypeDescriptor(configuration.getObjectMapperWrapper()) + ); + } + + public JsonType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + public JsonType(ObjectMapper objectMapper) { + super( + Object.class, + new JsonJdbcTypeDescriptor(), + new JsonJavaTypeDescriptor(new ObjectMapperWrapper(objectMapper)) + ); + } + + public JsonType(ObjectMapperWrapper objectMapperWrapper) { + super( + Object.class, + new JsonJdbcTypeDescriptor(), + new JsonJavaTypeDescriptor(objectMapperWrapper) + ); + } + + public JsonType(ObjectMapper objectMapper, Type javaType) { + super( + Object.class, + new JsonJdbcTypeDescriptor(), + new JsonJavaTypeDescriptor(new ObjectMapperWrapper(objectMapper), javaType) + ); + } + + public JsonType(ObjectMapperWrapper objectMapperWrapper, Type javaType) { + super( + Object.class, + new JsonJdbcTypeDescriptor(), + new JsonJavaTypeDescriptor(objectMapperWrapper, javaType) + ); + } + + public String getName() { + return "json"; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/AbstractJsonJdbcTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/AbstractJsonJdbcTypeDescriptor.java new file mode 100644 index 000000000..80e54e2f9 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/AbstractJsonJdbcTypeDescriptor.java @@ -0,0 +1,55 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcType; + +import java.sql.CallableStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractJsonJdbcTypeDescriptor implements JdbcType { + + @Override + public int getJdbcTypeCode() { + return Types.OTHER; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new BasicExtractor(javaType, this) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return javaType.wrap(extractJson(rs, paramIndex), options); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return javaType.wrap(extractJson(statement, index), options); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return javaType.wrap(extractJson(statement, name), options); + } + }; + } + + protected Object extractJson(ResultSet rs, int paramIndex) throws SQLException { + return rs.getObject(paramIndex); + } + + protected Object extractJson(CallableStatement statement, int index) throws SQLException { + return statement.getObject(index); + } + + protected Object extractJson(CallableStatement statement, String name) throws SQLException { + return statement.getObject(name); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JacksonUtil.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JacksonUtil.java new file mode 100644 index 000000000..05770cc49 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JacksonUtil.java @@ -0,0 +1,32 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; + +import java.lang.reflect.Type; + +/** + * @author Vlad Mihalcea + */ +public class JacksonUtil { + + public static T fromString(String string, Class clazz) { + return ObjectMapperWrapper.INSTANCE.fromString(string, clazz); + } + + public static T fromString(String string, Type type) { + return ObjectMapperWrapper.INSTANCE.fromString(string, type); + } + + public static String toString(Object value) { + return ObjectMapperWrapper.INSTANCE.toString(value); + } + + public static JsonNode toJsonNode(String value) { + return ObjectMapperWrapper.INSTANCE.toJsonNode(value); + } + + public static T clone(T value) { + return ObjectMapperWrapper.INSTANCE.clone(value); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBinaryJdbcTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBinaryJdbcTypeDescriptor.java new file mode 100644 index 000000000..dfda74f73 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBinaryJdbcTypeDescriptor.java @@ -0,0 +1,35 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import com.fasterxml.jackson.databind.JsonNode; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * @author Vlad Mihalcea + */ +public class JsonBinaryJdbcTypeDescriptor extends AbstractJsonJdbcTypeDescriptor { + + public static final JsonBinaryJdbcTypeDescriptor INSTANCE = new JsonBinaryJdbcTypeDescriptor(); + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder(javaType, this) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + st.setObject(index, javaType.unwrap(value, JsonNode.class, options), getJdbcTypeCode()); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + st.setObject(name, javaType.unwrap(value, JsonNode.class, options), getJdbcTypeCode()); + } + }; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesJdbcTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesJdbcTypeDescriptor.java new file mode 100644 index 000000000..51eecc9a6 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonBytesJdbcTypeDescriptor.java @@ -0,0 +1,74 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; + +import java.io.UnsupportedEncodingException; +import java.sql.*; + +/** + * @author Vlad Mihalcea + */ +public class JsonBytesJdbcTypeDescriptor extends AbstractJsonJdbcTypeDescriptor { + + public static final JsonBytesJdbcTypeDescriptor INSTANCE = new JsonBytesJdbcTypeDescriptor(); + + public static final String CHARSET = "UTF8"; + + @Override + public int getJdbcTypeCode() { + return Types.BINARY; + } + + @Override + public ValueBinder getBinder(final JavaType JavaType) { + return new BasicBinder(JavaType, this) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + st.setBytes(index, toJsonBytes(JavaType.unwrap(value, String.class, options))); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + st.setBytes(name, toJsonBytes(JavaType.unwrap(value, String.class, options))); + } + }; + } + + @Override + protected Object extractJson(ResultSet rs, int paramIndex) throws SQLException { + return fromJsonBytes(rs.getBytes(paramIndex)); + } + + @Override + protected Object extractJson(CallableStatement statement, int index) throws SQLException { + return fromJsonBytes(statement.getBytes(index)); + } + + @Override + protected Object extractJson(CallableStatement statement, String name) throws SQLException { + return fromJsonBytes(statement.getBytes(name)); + } + + protected byte[] toJsonBytes(String jsonValue) { + try { + return jsonValue.getBytes(CHARSET); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + + protected String fromJsonBytes(byte[] jsonBytes) { + if (jsonBytes == null) { + return null; + } + try { + return new String(jsonBytes, CHARSET); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonJavaTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonJavaTypeDescriptor.java new file mode 100644 index 000000000..4c7a08369 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonJavaTypeDescriptor.java @@ -0,0 +1,204 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; +import com.vladmihalcea.hibernate.util.LogUtils; +import com.vladmihalcea.hibernate.util.ReflectionUtils; +import org.hibernate.HibernateException; +import org.hibernate.annotations.common.reflection.XProperty; +import org.hibernate.annotations.common.reflection.java.JavaXMember; +import org.hibernate.engine.jdbc.BinaryStream; +import org.hibernate.engine.jdbc.internal.BinaryStreamImpl; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; +import org.hibernate.type.descriptor.java.BlobJavaType; +import org.hibernate.type.descriptor.java.DataHelper; +import org.hibernate.type.descriptor.java.MutableMutabilityPlan; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.sql.Blob; +import java.sql.SQLException; +import java.util.*; + +/** + * @author Vlad Mihalcea + */ +public class JsonJavaTypeDescriptor + extends AbstractClassJavaType implements DynamicParameterizedType { + + private Type propertyType; + + private Class propertyClass; + + private ObjectMapperWrapper objectMapperWrapper; + + public JsonJavaTypeDescriptor() { + this(ObjectMapperWrapper.INSTANCE); + } + + public JsonJavaTypeDescriptor(Type type) { + this(); + setPropertyClass(type); + } + + public JsonJavaTypeDescriptor(final ObjectMapperWrapper objectMapperWrapper) { + super(Object.class, new MutableMutabilityPlan() { + @Override + protected Object deepCopyNotNull(Object value) { + return objectMapperWrapper.clone(value); + } + }); + this.objectMapperWrapper = objectMapperWrapper; + } + + public JsonJavaTypeDescriptor(final ObjectMapperWrapper objectMapperWrapper, Type type) { + this(objectMapperWrapper); + setPropertyClass(type); + } + + @Override + public void setParameterValues(Properties parameters) { + final XProperty xProperty = (XProperty) parameters.get(DynamicParameterizedType.XPROPERTY); + Type type = (xProperty instanceof JavaXMember) ? + ReflectionUtils.invokeGetter(xProperty, "javaType") : + ((ParameterType) parameters.get(PARAMETER_TYPE)).getReturnedClass(); + setPropertyClass(type); + } + + @Override + public boolean areEqual(Object one, Object another) { + if (one == another) { + return true; + } + if (one == null || another == null) { + return false; + } + if (one instanceof String && another instanceof String) { + return one.equals(another); + } + if (one instanceof Collection && another instanceof Collection) { + return Objects.equals(one, another); + } + if (one.getClass().equals(another.getClass()) && + ReflectionUtils.getDeclaredMethodOrNull(one.getClass(), "equals", Object.class) != null) { + return one.equals(another); + } + return objectMapperWrapper.toJsonNode(objectMapperWrapper.toString(one)).equals( + objectMapperWrapper.toJsonNode(objectMapperWrapper.toString(another)) + ); + } + + @Override + public String toString(Object value) { + return objectMapperWrapper.toString(value); + } + + @Override + public Object fromString(CharSequence string) { + if (String.class.isAssignableFrom(propertyClass)) { + return string; + } + return objectMapperWrapper.fromString((String) string, propertyType); + } + + @SuppressWarnings({"unchecked"}) + @Override + public X unwrap(Object value, Class type, WrapperOptions options) { + if (value == null) { + return null; + } + + if (String.class.isAssignableFrom(type)) { + return value instanceof String ? (X) value : (X) toString(value); + } else if (BinaryStream.class.isAssignableFrom(type) || + byte[].class.isAssignableFrom(type)) { + String stringValue = (value instanceof String) ? (String) value : toString(value); + + return (X) new BinaryStreamImpl(DataHelper.extractBytes(new ByteArrayInputStream(stringValue.getBytes()))); + } else if (Blob.class.isAssignableFrom(type)) { + String stringValue = (value instanceof String) ? (String) value : toString(value); + + final Blob blob = BlobJavaType.INSTANCE.fromString(stringValue); + return (X) blob; + } else if (Object.class.isAssignableFrom(type)) { + String stringValue = (value instanceof String) ? (String) value : toString(value); + return (X) objectMapperWrapper.toJsonNode(stringValue); + } + + throw unknownUnwrap(type); + } + + @Override + public Object wrap(X value, WrapperOptions options) { + if (value == null) { + return null; + } + + Blob blob = null; + + if (Blob.class.isAssignableFrom(value.getClass())) { + blob = options.getLobCreator().wrap((Blob) value); + } else if (byte[].class.isAssignableFrom(value.getClass())) { + blob = options.getLobCreator().createBlob((byte[]) value); + } else if (InputStream.class.isAssignableFrom(value.getClass())) { + InputStream inputStream = (InputStream) value; + try { + blob = options.getLobCreator().createBlob(inputStream, inputStream.available()); + } catch (IOException e) { + throw unknownWrap(value.getClass()); + } + } + + String stringValue; + try { + stringValue = (blob != null) ? new String(DataHelper.extractBytes(blob.getBinaryStream())) : value.toString(); + } catch (SQLException e) { + throw new HibernateException("Unable to extract binary stream from Blob", e); + } + + return fromString(stringValue); + } + + private void setPropertyClass(Type type) { + this.propertyType = type; + if (type instanceof ParameterizedType) { + type = ((ParameterizedType) type).getRawType(); + } else if (type instanceof TypeVariable) { + type = ((TypeVariable) type).getGenericDeclaration().getClass(); + } + this.propertyClass = (Class) type; + validatePropertyType(); + } + + private void validatePropertyType() { + if(Collection.class.isAssignableFrom(propertyClass)) { + if (propertyType instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) propertyType; + + for(Class genericType : ReflectionUtils.getGenericTypes(parameterizedType)) { + if(validatedTypes.contains(genericType)) { + continue; + } + validatedTypes.add(genericType); + Method equalsMethod = ReflectionUtils.getMethodOrNull(genericType, "equals", Object.class); + Method hashCodeMethod = ReflectionUtils.getMethodOrNull(genericType, "hashCode"); + + if(equalsMethod == null || + hashCodeMethod == null || + Object.class.equals(equalsMethod.getDeclaringClass()) || + Object.class.equals(hashCodeMethod.getDeclaringClass())) { + LogUtils.LOGGER.warn("The {} class should override both the equals and hashCode methods based on the JSON object value it represents!", genericType); + } + } + } + } + } + + private static List validatedTypes = new ArrayList<>(); +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonJdbcTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonJdbcTypeDescriptor.java new file mode 100644 index 000000000..cb9e4ec5e --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonJdbcTypeDescriptor.java @@ -0,0 +1,84 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver; +import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; + +import java.sql.*; + +/** + * @author Vlad Mihalcea + */ +public class JsonJdbcTypeDescriptor extends AbstractJsonJdbcTypeDescriptor { + + private volatile Dialect dialect; + private volatile AbstractJsonJdbcTypeDescriptor sqlTypeDescriptor; + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder(javaType, this) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + sqlTypeDescriptor(st.getConnection()).getBinder(javaType).bind( + st, value, index, options + ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + sqlTypeDescriptor(st.getConnection()).getBinder(javaType).bind( + st, value, name, options + ); + } + }; + } + + @Override + protected Object extractJson(ResultSet rs, int paramIndex) throws SQLException { + return sqlTypeDescriptor(rs.getStatement().getConnection()).extractJson(rs, paramIndex); + } + + @Override + protected Object extractJson(CallableStatement statement, int index) throws SQLException { + return sqlTypeDescriptor(statement.getConnection()).extractJson(statement, index); + } + + @Override + protected Object extractJson(CallableStatement statement, String name) throws SQLException { + return sqlTypeDescriptor(statement.getConnection()).extractJson(statement, name); + } + + private AbstractJsonJdbcTypeDescriptor sqlTypeDescriptor(Connection connection) { + if(sqlTypeDescriptor == null) { + sqlTypeDescriptor = resolveSqlTypeDescriptor(connection); + } + return sqlTypeDescriptor; + } + + private AbstractJsonJdbcTypeDescriptor resolveSqlTypeDescriptor(Connection connection) { + try { + StandardDialectResolver dialectResolver = new StandardDialectResolver(); + dialect = dialectResolver.resolveDialect( + new DatabaseMetaDataDialectResolutionInfoAdapter(connection.getMetaData()) + ); + if(PostgreSQLDialect.class.isInstance(dialect)) { + return JsonBinaryJdbcTypeDescriptor.INSTANCE; + } else if(H2Dialect.class.isInstance(dialect)) { + return JsonBytesJdbcTypeDescriptor.INSTANCE; + } else { + return JsonStringJdbcTypeDescriptor.INSTANCE; + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonNodeJavaTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonNodeJavaTypeDescriptor.java new file mode 100644 index 000000000..5925d27d6 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonNodeJavaTypeDescriptor.java @@ -0,0 +1,91 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; +import org.hibernate.SharedSessionContract; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; +import org.hibernate.type.descriptor.java.MutableMutabilityPlan; + +import java.io.Serializable; + +/** + * @author Vlad Mihalcea + */ +public class JsonNodeJavaTypeDescriptor + extends AbstractClassJavaType { + + public static final JsonNodeJavaTypeDescriptor INSTANCE = new JsonNodeJavaTypeDescriptor(); + + private ObjectMapperWrapper objectMapperWrapper; + + public JsonNodeJavaTypeDescriptor() { + this(ObjectMapperWrapper.INSTANCE); + } + + public JsonNodeJavaTypeDescriptor(final ObjectMapperWrapper objectMapperWrapper) { + super(JsonNode.class, new MutableMutabilityPlan() { + @Override + public Serializable disassemble(JsonNode value, SharedSessionContract session) { + return JacksonUtil.toString(value); + } + + @Override + public JsonNode assemble(Serializable cached, SharedSessionContract session) { + return JacksonUtil.toJsonNode((String) cached); + } + + @Override + protected JsonNode deepCopyNotNull(JsonNode value) { + return objectMapperWrapper.clone(value); + } + }); + this.objectMapperWrapper = objectMapperWrapper; + } + + @Override + public boolean areEqual(JsonNode one, JsonNode another) { + if (one == another) { + return true; + } + if (one == null || another == null) { + return false; + } + return objectMapperWrapper.toJsonNode(objectMapperWrapper.toString(one)).equals( + objectMapperWrapper.toJsonNode(objectMapperWrapper.toString(another))); + } + + @Override + public String toString(JsonNode value) { + return objectMapperWrapper.toString(value); + } + + @Override + public JsonNode fromString(CharSequence string) { + return objectMapperWrapper.toJsonNode((String) string); + } + + @SuppressWarnings({"unchecked"}) + @Override + public X unwrap(JsonNode value, Class type, WrapperOptions options) { + if (value == null) { + return null; + } + if (String.class.isAssignableFrom(type)) { + return (X) toString(value); + } + if (JsonNode.class.isAssignableFrom(type)) { + return (X) objectMapperWrapper.toJsonNode(toString(value)); + } + throw unknownUnwrap(type); + } + + @Override + public JsonNode wrap(X value, WrapperOptions options) { + if (value == null) { + return null; + } + return fromString(value.toString()); + } + +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonStringJdbcTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonStringJdbcTypeDescriptor.java new file mode 100644 index 000000000..00d0ddbde --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/json/internal/JsonStringJdbcTypeDescriptor.java @@ -0,0 +1,52 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; + +import java.sql.*; + +/** + * @author Vlad Mihalcea + */ +public class JsonStringJdbcTypeDescriptor extends AbstractJsonJdbcTypeDescriptor { + + public static final JsonStringJdbcTypeDescriptor INSTANCE = new JsonStringJdbcTypeDescriptor(); + + @Override + public int getJdbcTypeCode() { + return Types.VARCHAR; + } + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder(javaType, this) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + st.setString(index, javaType.unwrap(value, String.class, options)); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + st.setString(name, javaType.unwrap(value, String.class, options)); + } + }; + } + + @Override + protected Object extractJson(ResultSet rs, int paramIndex) throws SQLException { + return rs.getString(paramIndex); + } + + @Override + protected Object extractJson(CallableStatement statement, int index) throws SQLException { + return statement.getString(index); + } + + @Override + protected Object extractJson(CallableStatement statement, String name) throws SQLException { + return statement.getString(name); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/range/PostgreSQLRangeType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/range/PostgreSQLRangeType.java new file mode 100644 index 000000000..714590891 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/range/PostgreSQLRangeType.java @@ -0,0 +1,147 @@ +package com.vladmihalcea.hibernate.type.range; + +import com.vladmihalcea.hibernate.type.ImmutableType; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.util.ReflectionUtils; +import org.hibernate.HibernateException; +import org.hibernate.annotations.common.reflection.XProperty; +import org.hibernate.annotations.common.reflection.java.JavaXMember; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.usertype.DynamicParameterizedType; +import org.postgresql.util.PGobject; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.Properties; + +/** + * Maps a {@link Range} object type to a PostgreSQL range + * column type. + *

+ * Supported range types: + *

    + *
  • int4range
  • + *
  • int8range
  • + *
  • numrange
  • + *
  • tsrange
  • + *
  • tstzrange
  • + *
  • daterange
  • + *
+ *

+ * For more details about how to use it, + * check out this article + * on vladmihalcea.com. + * + * @author Edgar Asatryan + * @author Vlad Mihalcea + */ +public class PostgreSQLRangeType extends ImmutableType implements DynamicParameterizedType { + + public static final PostgreSQLRangeType INSTANCE = new PostgreSQLRangeType(); + + private Type type; + + public PostgreSQLRangeType() { + super(Range.class); + } + + public PostgreSQLRangeType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + super(Range.class, new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + @Override + public int getSqlType() { + return Types.OTHER; + } + + @Override + protected Range get(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException { + PGobject pgObject = (PGobject) rs.getObject(position); + + if (pgObject == null) { + return null; + } + + String type = pgObject.getType(); + String value = pgObject.getValue(); + + switch (type) { + case "int4range": + return Range.integerRange(value); + case "int8range": + return Range.longRange(value); + case "numrange": + return Range.bigDecimalRange(value); + case "tsrange": + return Range.localDateTimeRange(value); + case "tstzrange": + return Range.zonedDateTimeRange(value); + case "daterange": + return Range.localDateRange(value); + default: + throw new HibernateException( + new IllegalStateException("The range type [" + type + "] is not supported!") + ); + } + } + + @Override + protected void set(PreparedStatement st, Range range, int index, SharedSessionContractImplementor session) throws SQLException { + + if (range == null) { + st.setNull(index, Types.OTHER); + } else { + PGobject object = new PGobject(); + object.setType(determineRangeType(range)); + object.setValue(range.asString()); + + st.setObject(index, object); + } + } + + private static String determineRangeType(Range range) { + Class clazz = range.getClazz(); + + if (clazz.equals(Integer.class)) { + return "int4range"; + } else if (clazz.equals(Long.class)) { + return "int8range"; + } else if (clazz.equals(BigDecimal.class)) { + return "numrange"; + } else if (clazz.equals(LocalDateTime.class)) { + return "tsrange"; + } else if (clazz.equals(ZonedDateTime.class)) { + return "tstzrange"; + } else if (clazz.equals(LocalDate.class)) { + return "daterange"; + } + + throw new HibernateException( + new IllegalStateException("The class [" + clazz.getName() + "] is not supported!") + ); + } + + @Override + public void setParameterValues(Properties parameters) { + final XProperty xProperty = (XProperty) parameters.get(DynamicParameterizedType.XPROPERTY); + if (xProperty instanceof JavaXMember) { + type = ReflectionUtils.invokeGetter(xProperty, "javaType"); + } else { + type = ((ParameterType) parameters.get(PARAMETER_TYPE)).getReturnedClass(); + } + } + + public Class getElementType() { + return type instanceof ParameterizedType ? + (Class) ((ParameterizedType) type).getActualTypeArguments()[0] : null; + } + +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/range/Range.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/range/Range.java new file mode 100644 index 000000000..aff8356e2 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/range/Range.java @@ -0,0 +1,639 @@ +package com.vladmihalcea.hibernate.type.range; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.util.Objects; +import java.util.function.Function; + +/** + * Represents the range/interval with two bounds. Abstraction follows the semantics of the mathematical interval. The + * range can be unbounded or open from the left or/and unbounded from the right. The range supports half-open or closed + * bounds on both sides. + * + *

+ * The class has some very simple methods for usability. For example {@link Range#contains(Comparable)} method can tell user whether + * this range contains argument or not. The {@link Range#contains(Range)} helps to find out whether this range fully + * enclosing argument or not. + *

+ * For more details about how to use it, + * check out this article + * on vladmihalcea.com. + * + * @author Edgar Asatryan + * @author Vlad Mihalcea + */ +public final class Range implements Serializable { + + public static final int LOWER_INCLUSIVE = 1 << 1; + public static final int LOWER_EXCLUSIVE = 1 << 2; + public static final int UPPER_INCLUSIVE = 1 << 3; + public static final int UPPER_EXCLUSIVE = 1 << 4; + public static final int LOWER_INFINITE = (1 << 5) | LOWER_EXCLUSIVE; + public static final int UPPER_INFINITE = (1 << 6) | UPPER_EXCLUSIVE; + + public static final String EMPTY = "empty"; + + public static final String INFINITY = "infinity"; + + private static final DateTimeFormatter LOCAL_DATE_TIME = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd HH:mm:ss") + .optionalStart() + .appendPattern(".") + .appendFraction(ChronoField.NANO_OF_SECOND, 1, 6, false) + .optionalEnd() + .toFormatter(); + + private static final DateTimeFormatter ZONE_DATE_TIME = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd HH:mm:ss") + .optionalStart() + .appendPattern(".") + .appendFraction(ChronoField.NANO_OF_SECOND, 1, 6, false) + .optionalEnd() + .appendOffset("+HH:mm", "Z") + .toFormatter(); + + private final T lower; + private final T upper; + private final int mask; + private final Class clazz; + + private Range(T lower, T upper, int mask, Class clazz) { + this.lower = lower; + this.upper = upper; + this.mask = mask; + this.clazz = clazz; + + if (isBounded() && lower.compareTo(upper) > 0) { + throw new IllegalArgumentException("The lower bound is greater then upper!"); + } + } + + /** + * Creates the closed range with provided bounds. + *

+ * The mathematical equivalent will be: + *

{@code
+     *     [a, b] = {x | a <= x <= b}
+     * }
. + * + * @param lower The lower bound, never null. + * @param upper The upper bound, never null. + * @param The type of bounds. + * + * @return The closed range. + */ + @SuppressWarnings("unchecked") + public static > Range closed(T lower, T upper) { + Objects.requireNonNull(lower); + Objects.requireNonNull(upper); + return new Range<>(lower, upper, LOWER_INCLUSIVE | UPPER_INCLUSIVE, (Class) lower.getClass()); + } + + /** + * Creates the open range with provided bounds. + *

+ * The mathematical equivalent will be: + *

{@code
+     *     (a, b) = {x | a < x < b}
+     * }
+ * + * @param lower The lower bound, never null. + * @param upper The upper bound, never null. + * @param The type of bounds. + * + * @return The range. + */ + @SuppressWarnings("unchecked") + public static > Range open(T lower, T upper) { + Objects.requireNonNull(lower); + Objects.requireNonNull(upper); + return new Range<>(lower, upper, LOWER_EXCLUSIVE | UPPER_EXCLUSIVE, (Class) lower.getClass()); + } + + /** + * Creates the left-open, right-closed range with provided bounds. + *

+ * The mathematical equivalent will be: + *

{@code
+     *     (a, b] = {x | a < x <= b}
+     * }
+ * + * @param lower The lower bound, never null. + * @param upper The upper bound, never null. + * @param The type of bounds. + * + * @return The range. + */ + @SuppressWarnings("unchecked") + public static > Range openClosed(T lower, T upper) { + Objects.requireNonNull(lower); + Objects.requireNonNull(upper); + return new Range<>(lower, upper, LOWER_EXCLUSIVE | UPPER_INCLUSIVE, (Class) lower.getClass()); + } + + /** + * Creates the left-closed, right-open range with provided bounds. + *

+ * The mathematical equivalent will be: + *

{@code
+     *     [a, b) = {x | a <= x < b}
+     * }
+ * + * @param lower The lower bound, never null. + * @param upper The upper bound, never null. + * @param The type of bounds. + * + * @return The range. + */ + @SuppressWarnings("unchecked") + public static > Range closedOpen(T lower, T upper) { + Objects.requireNonNull(lower); + Objects.requireNonNull(upper); + return new Range<>(lower, upper, LOWER_INCLUSIVE | UPPER_EXCLUSIVE, (Class) lower.getClass()); + } + + /** + * Creates the left-bounded, left-open and right-unbounded range with provided lower bound. + *

+ * The mathematical equivalent will be: + *

{@code
+     *     (a, +∞) = {x | x > a}
+     * }
+ * + * @param lower The lower bound, never null. + * @param The type of bounds. + * + * @return The range. + */ + @SuppressWarnings("unchecked") + public static > Range openInfinite(T lower) { + Objects.requireNonNull(lower); + return new Range<>(lower, null, LOWER_EXCLUSIVE | UPPER_INFINITE, (Class) lower.getClass()); + } + + /** + * Creates the left-bounded, left-closed and right-unbounded range with provided lower bound. + *

+ * The mathematical equivalent will be: + *

{@code
+     *     [a, +∞) = {x | x >= a}
+     * }
+ * + * @param lower The lower bound, never null. + * @param The type of bounds. + * + * @return The range. + */ + @SuppressWarnings("unchecked") + public static > Range closedInfinite(T lower) { + Objects.requireNonNull(lower); + return new Range(lower, null, LOWER_INCLUSIVE | UPPER_INFINITE, lower.getClass()); + } + + /** + * Creates the left-unbounded, right-bounded and right-open range with provided upper bound. + *

+ * The mathematical equivalent will be: + *

{@code
+     *     (-∞, b) = {x | x < b}
+     * }
+ * + * @param upper The upper bound, never null. + * @param The type of bounds. + * + * @return The range. + */ + @SuppressWarnings("unchecked") + public static > Range infiniteOpen(T upper) { + Objects.requireNonNull(upper); + return new Range<>(null, upper, UPPER_EXCLUSIVE | LOWER_INFINITE, (Class) upper.getClass()); + } + + /** + * Creates the left-unbounded, right-bounded and right-closed range with provided upper bound. + *

+ * The mathematical equivalent will be: + *

{@code
+     *     (-∞, b] = {x | x =< b}
+     * }
+ * + * @param upper The upper bound, never null. + * @param The type of bounds. + * + * @return The range. + */ + @SuppressWarnings("unchecked") + public static > Range infiniteClosed(T upper) { + Objects.requireNonNull(upper); + return new Range<>(null, upper, UPPER_INCLUSIVE | LOWER_INFINITE, (Class) upper.getClass()); + } + + /** + * Creates the unbounded at both ends range with provided upper bound. + *

+ * The mathematical equivalent will be: + *

{@code
+     *     (-∞, +∞) = ℝ
+     * }
+ * + * @param cls The range class, never null. + * @param The type of bounds. + * + * @return The infinite range. + */ + @SuppressWarnings("unchecked") + public static > Range infinite(Class cls) { + return new Range<>(null, null, LOWER_INFINITE | UPPER_INFINITE, cls); + } + + @SuppressWarnings("unchecked") + public static Range ofString(String str, Function converter, Class clazz) { + if(str.equals(EMPTY)) { + return emptyRange(clazz); + } + + int mask = str.charAt(0) == '[' ? LOWER_INCLUSIVE : LOWER_EXCLUSIVE; + mask |= str.charAt(str.length() - 1) == ']' ? UPPER_INCLUSIVE : UPPER_EXCLUSIVE; + + int delim = str.indexOf(','); + + if (delim == -1) { + throw new IllegalArgumentException("Cannot find comma character"); + } + + String lowerStr = str.substring(1, delim); + String upperStr = str.substring(delim + 1, str.length() - 1); + + if (lowerStr.length() == 0 || lowerStr.endsWith(INFINITY)) { + mask |= LOWER_INFINITE; + } + + if (upperStr.length() == 0 || upperStr.endsWith(INFINITY)) { + mask |= UPPER_INFINITE; + } + + T lower = null; + T upper = null; + + if ((mask & LOWER_INFINITE) != LOWER_INFINITE) { + lower = converter.apply(lowerStr); + } + + if ((mask & UPPER_INFINITE) != UPPER_INFINITE) { + upper = converter.apply(upperStr); + } + + return new Range<>(lower, upper, mask, clazz); + } + + /** + * Creates the {@code BigDecimal} range from provided string: + *
{@code
+     *     Range closed = Range.bigDecimalRange("[0.1,1.1]");
+     *     Range halfOpen = Range.bigDecimalRange("(0.1,1.1]");
+     *     Range open = Range.bigDecimalRange("(0.1,1.1)");
+     *     Range leftUnbounded = Range.bigDecimalRange("(,1.1)");
+     * }
+ * + * @param range The range string, for example {@literal "[5.5,7.8]"}. + * + * @return The range of {@code BigDecimal}s. + * + * @throws NumberFormatException when one of the bounds are invalid. + */ + public static Range bigDecimalRange(String range) { + return ofString(range, BigDecimal::new, BigDecimal.class); + } + + /** + * Creates the {@code Integer} range from provided string: + *
{@code
+     *     Range closed = Range.integerRange("[1,5]");
+     *     Range halfOpen = Range.integerRange("(-1,1]");
+     *     Range open = Range.integerRange("(1,2)");
+     *     Range leftUnbounded = Range.integerRange("(,10)");
+     *     Range unbounded = Range.integerRange("(,)");
+     * }
+ * + * @param range The range string, for example {@literal "[5,7]"}. + * + * @return The range of {@code Integer}s. + * + * @throws NumberFormatException when one of the bounds are invalid. + */ + public static Range integerRange(String range) { + return ofString(range, Integer::parseInt, Integer.class); + } + + /** + * Creates the {@code Long} range from provided string: + *
{@code
+     *     Range closed = Range.longRange("[1,5]");
+     *     Range halfOpen = Range.longRange("(-1,1]");
+     *     Range open = Range.longRange("(1,2)");
+     *     Range leftUnbounded = Range.longRange("(,10)");
+     *     Range unbounded = Range.longRange("(,)");
+     * }
+ * + * @param range The range string, for example {@literal "[5,7]"}. + * + * @return The range of {@code Long}s. + * + * @throws NumberFormatException when one of the bounds are invalid. + */ + public static Range longRange(String range) { + return ofString(range, Long::parseLong, Long.class); + } + + /** + * Creates the {@code LocalDateTime} range from provided string: + *
{@code
+     *     Range closed = Range.localDateTimeRange("[2014-04-28 16:00:49,2015-04-28 16:00:49]");
+     *     Range quoted = Range.localDateTimeRange("[\"2014-04-28 16:00:49\",\"2015-04-28 16:00:49\"]");
+     *     Range iso = Range.localDateTimeRange("[\"2014-04-28T16:00:49.2358\",\"2015-04-28T16:00:49\"]");
+     * }
+ *

+ * The valid formats for bounds are: + *

    + *
  • yyyy-MM-dd HH:mm:ss[.SSSSSS]
  • + *
  • yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]
  • + *
+ * + * @param range The range string, for example {@literal "[2014-04-28 16:00:49,2015-04-28 16:00:49]"}. + * + * @return The range of {@code LocalDateTime}s. + * + * @throws DateTimeParseException when one of the bounds are invalid. + */ + public static Range localDateTimeRange(String range) { + return ofString(range, parseLocalDateTime().compose(unquote()), LocalDateTime.class); + } + + /** + * Creates the {@code LocalDate} range from provided string: + *
{@code
+     *     Range closed = Range.localDateRange("[2014-04-28,2015-04-289]");
+     *     Range quoted = Range.localDateRange("[\"2014-04-28\",\"2015-04-28\"]");
+     *     Range iso = Range.localDateRange("[\"2014-04-28\",\"2015-04-28\"]");
+     * }
+ *

+ * The valid formats for bounds are: + *

    + *
  • yyyy-MM-dd
  • + *
  • yyyy-MM-dd
  • + *
+ * + * @param range The range string, for example {@literal "[2014-04-28,2015-04-28]"}. + * + * @return The range of {@code LocalDate}s. + * + * @throws DateTimeParseException when one of the bounds are invalid. + */ + public static Range localDateRange(String range) { + Function parseLocalDate = LocalDate::parse; + return ofString(range, parseLocalDate.compose(unquote()), LocalDate.class); + } + + /** + * Creates the {@code ZonedDateTime} range from provided string: + *
{@code
+     *     Range closed = Range.zonedDateTimeRange("[2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00]");
+     *     Range quoted = Range.zonedDateTimeRange("[\"2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00\"]");
+     *     Range iso = Range.zonedDateTimeRange("[2011-12-03T10:15:30+01:00[Europe/Paris], 2012-12-03T10:15:30+01:00[Europe/Paris]]");
+     * }
+ *

+ * The valid formats for bounds are: + *

    + *
  • yyyy-MM-dd HH:mm:ss[.SSSSSS]X
  • + *
  • yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]X
  • + *
+ * + * @param rangeStr The range string, for example {@literal "[2011-12-03T10:15:30+01:00,2012-12-03T10:15:30+01:00]"}. + * + * @return The range of {@code ZonedDateTime}s. + * + * @throws DateTimeParseException when one of the bounds are invalid. + * @throws IllegalArgumentException when bounds time zones are different. + */ + public static Range zonedDateTimeRange(String rangeStr) { + Range range = ofString(rangeStr, parseZonedDateTime().compose(unquote()), ZonedDateTime.class); + if (range.hasLowerBound() && range.hasUpperBound()) { + ZoneId lowerZone = range.lower().getZone(); + ZoneId upperZone = range.upper().getZone(); + if (!lowerZone.equals(upperZone)) { + Duration lowerDst = ZoneId.systemDefault().getRules().getDaylightSavings(range.lower().toInstant()); + Duration upperDst = ZoneId.systemDefault().getRules().getDaylightSavings(range.upper().toInstant()); + long dstSeconds = upperDst.minus(lowerDst).getSeconds(); + if(dstSeconds < 0 ) { + dstSeconds *= -1; + } + long zoneDriftSeconds = ((ZoneOffset) lowerZone).getTotalSeconds() - ((ZoneOffset) upperZone).getTotalSeconds(); + if (zoneDriftSeconds < 0) { + zoneDriftSeconds *= -1; + } + + if (dstSeconds != zoneDriftSeconds) { + throw new IllegalArgumentException("The upper and lower bounds must be in same time zone!"); + } + } + } + return range; + } + + private static Function parseLocalDateTime() { + return s -> { + try { + return LocalDateTime.parse(s, LOCAL_DATE_TIME); + } catch (DateTimeParseException e) { + return LocalDateTime.parse(s); + } + }; + } + + private static Function parseZonedDateTime() { + return s -> { + try { + return ZonedDateTime.parse(s, ZONE_DATE_TIME); + } catch (DateTimeParseException e) { + return ZonedDateTime.parse(s); + } + }; + } + + private static Function unquote() { + return s -> { + if (s.charAt(0) == '\"' && s.charAt(s.length() - 1) == '\"') { + return s.substring(1, s.length() - 1); + } + + return s; + }; + } + + private boolean isBounded() { + return !hasMask(LOWER_INFINITE) && !hasMask(UPPER_INFINITE); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Range)) return false; + Range range = (Range) o; + return mask == range.mask && + Objects.equals(lower, range.lower) && + Objects.equals(upper, range.upper) && + Objects.equals(clazz, range.clazz); + } + + @Override + public int hashCode() { + return Objects.hash(lower, upper, mask, clazz); + } + + @Override + public String toString() { + return "Range{" + "lower=" + lower + + ", upper=" + upper + + ", mask=" + mask + + ", clazz=" + clazz + + '}'; + } + + public boolean hasMask(int flag) { + return (mask & flag) == flag; + } + + public boolean isLowerBoundClosed() { + return hasLowerBound() && hasMask(LOWER_INCLUSIVE); + } + + public boolean isUpperBoundClosed() { + return hasUpperBound() && hasMask(UPPER_INCLUSIVE); + } + + public boolean hasLowerBound() { + return !hasMask(LOWER_INFINITE); + } + + public boolean hasUpperBound() { + return !hasMask(UPPER_INFINITE); + } + + /** + * Returns the lower bound of this range. If {@code null} is returned then this range is left-unbounded. + * + * @return The lower bound. + */ + public T lower() { + return lower; + } + + /** + * Returns the upper bound of this range. If {@code null} is returned then this range is right-unbounded. + * + * @return The upper bound. + */ + public T upper() { + return upper; + } + + /** + * Determines whether this range contains this point or not. + *

+ * For example: + *

{@code
+     *     assertTrue(integerRange("[1,2]").contains(1))
+     *     assertTrue(integerRange("[1,2]").contains(2))
+     *     assertTrue(integerRange("[-1,1]").contains(0))
+     *     assertTrue(infinity(Integer.class).contains(Integer.MAX_VALUE))
+     *     assertTrue(infinity(Integer.class).contains(Integer.MIN_VALUE))
+     *
+     *     assertFalse(integerRange("(1,2]").contains(1))
+     *     assertFalse(integerRange("(1,2]").contains(3))
+     *     assertFalse(integerRange("[-1,1]").contains(0))
+     * }
+ * + * @param point The point to check. + * + * @return Whether {@code point} in this range or not. + */ + @SuppressWarnings("unchecked") + public boolean contains(T point) { + boolean l = hasLowerBound(); + boolean u = hasUpperBound(); + + if (l && u) { + boolean inLower = hasMask(LOWER_INCLUSIVE) ? lower.compareTo(point) <= 0 : lower.compareTo(point) < 0; + boolean inUpper = hasMask(UPPER_INCLUSIVE) ? upper.compareTo(point) >= 0 : upper.compareTo(point) > 0; + + return inLower && inUpper; + } else if (l) { + return hasMask(LOWER_INCLUSIVE) ? lower.compareTo(point) <= 0 : lower.compareTo(point) < 0; + } else if (u) { + return hasMask(UPPER_INCLUSIVE) ? upper.compareTo(point) >= 0 : upper.compareTo(point) > 0; + } + + // INFINITY + return true; + } + + /** + * Determines whether this range contains this point or not. + *

+ * For example: + *

{@code
+     *     assertTrue(integerRange("[-2,2]").contains(integerRange("[-1,1]")))
+     *     assertTrue(integerRange("(,)").contains(integerRange("(,)"))
+     *
+     *     assertFalse(integerRange("[-2,2)").contains(integerRange("[-1,2]")))
+     *     assertFalse(integerRange("(-2,2]").contains(1))
+     * }
+ * + * @param range The range to check. + * + * @return Whether {@code range} in this range or not. + */ + public boolean contains(Range range) { + return (!range.hasLowerBound() || contains(range.lower)) && (!range.hasUpperBound() || contains(range.upper)); + } + + public String asString() { + StringBuilder sb = new StringBuilder(); + + sb.append(hasMask(LOWER_INCLUSIVE) ? '[' : '(') + .append(hasLowerBound() ? boundToString().apply(lower) : "") + .append(",") + .append(hasUpperBound() ? boundToString().apply(upper) : "") + .append(hasMask(UPPER_INCLUSIVE) ? ']' : ')'); + + return sb.toString(); + } + + private Function boundToString() { + return t -> { + if (clazz.equals(ZonedDateTime.class)) { + return ZONE_DATE_TIME.format((ZonedDateTime) t); + } + + return t.toString(); + }; + } + + Class getClazz() { + return clazz; + } + + public static > Range emptyRange(Class clazz) { + return new Range( + null, + null, + LOWER_INFINITE|UPPER_INFINITE, + clazz + ); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/range/guava/PostgreSQLGuavaRangeType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/range/guava/PostgreSQLGuavaRangeType.java new file mode 100644 index 000000000..befd0ea50 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/range/guava/PostgreSQLGuavaRangeType.java @@ -0,0 +1,532 @@ +package com.vladmihalcea.hibernate.type.range.guava; + +import com.google.common.collect.BoundType; +import com.google.common.collect.Range; +import com.vladmihalcea.hibernate.type.ImmutableType; +import com.vladmihalcea.hibernate.util.ReflectionUtils; +import org.hibernate.HibernateException; +import org.hibernate.annotations.common.reflection.XProperty; +import org.hibernate.annotations.common.reflection.java.JavaXMember; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.usertype.DynamicParameterizedType; +import org.postgresql.util.PGobject; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.util.Properties; +import java.util.function.Function; + +/** + * Maps a {@link Range} object type to a PostgreSQL range + * column type. + *

+ * Supported range types: + *

    + *
  • int4range
  • + *
  • int8range
  • + *
  • numrange
  • + *
  • tsrange
  • + *
  • tstzrange
  • + *
  • daterange
  • + *
+ * + * @author Edgar Asatryan + * @author Vlad Mihalcea + * @author Jan-Willem Gmelig Meyling + */ +public class PostgreSQLGuavaRangeType extends ImmutableType implements DynamicParameterizedType { + + /** + * An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to. + */ + private static final Range EMPTY_INT_RANGE = Range.closedOpen(Integer.MIN_VALUE, Integer.MIN_VALUE); + + /** + * An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to. + */ + private static final Range EMPTY_LONG_RANGE = Range.closedOpen(Long.MIN_VALUE, Long.MIN_VALUE); + + /** + * An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to. + */ + private static final Range EMPTY_BIGDECIMAL_RANGE = Range.closedOpen(BigDecimal.ZERO, BigDecimal.ZERO); + + /** + * An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to. + */ + private static final Range EMPTY_LOCALDATETIME_RANGE = Range.closedOpen(LocalDateTime.MIN, LocalDateTime.MIN); + + /** + * An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to. + */ + private static final Range EMPTY_OFFSETDATETIME_RANGE = Range.closedOpen(OffsetDateTime.MIN, OffsetDateTime.MIN); + + /** + * An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to. + */ + private static final Range EMPTY_ZONEDDATETIME_RANGE = Range.closedOpen(OffsetDateTime.MIN.toZonedDateTime(), OffsetDateTime.MIN.toZonedDateTime()); + + /** + * An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to. + */ + private static final Range EMPTY_DATE_RANGE = Range.closedOpen(LocalDate.MIN, LocalDate.MIN); + + private static final DateTimeFormatter LOCAL_DATE_TIME = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd HH:mm:ss") + .optionalStart() + .appendPattern(".") + .appendFraction(ChronoField.NANO_OF_SECOND, 1, 6, false) + .optionalEnd() + .toFormatter(); + + private static final DateTimeFormatter OFFSET_DATE_TIME = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd HH:mm:ss") + .optionalStart() + .appendPattern(".") + .appendFraction(ChronoField.NANO_OF_SECOND, 1, 6, false) + .optionalEnd() + .appendPattern("X") + .toFormatter(); + + public static final PostgreSQLGuavaRangeType INSTANCE = new PostgreSQLGuavaRangeType(); + + private Type type; + + private Class elementType; + + public PostgreSQLGuavaRangeType() { + super(Range.class); + } + + public PostgreSQLGuavaRangeType(Class elementType) { + super(Range.class); + this.elementType = elementType; + } + + @Override + public int getSqlType() { + return Types.OTHER; + } + + @Override + protected Range get(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException { + PGobject pgObject = (PGobject) rs.getObject(position); + + if (pgObject == null) { + return null; + } + + String type = pgObject.getType(); + String value = pgObject.getValue(); + + switch (type) { + case "int4range": + return integerRange(value); + case "int8range": + return longRange(value); + case "numrange": + return bigDecimalRange(value); + case "tsrange": + return localDateTimeRange(value); + case "tstzrange": + return ZonedDateTime.class.equals(elementType) ? zonedDateTimeRange(value) : offsetDateTimeRange(value); + case "daterange": + return localDateRange(value); + default: + throw new HibernateException( + new IllegalStateException("The range type [" + type + "] is not supported!") + ); + } + } + + @Override + protected void set(PreparedStatement st, Range range, int index, SharedSessionContractImplementor session) throws SQLException { + if (range == null) { + st.setNull(index, Types.OTHER); + } else { + PGobject object = new PGobject(); + object.setType(determineRangeType(range)); + object.setValue(asString(range)); + st.setObject(index, object); + } + } + + private String determineRangeType(Range range) { + Type clazz = this.elementType; + + if (clazz == null) { + Object anyEndpoint = range.hasLowerBound() ? range.lowerEndpoint() : + range.hasUpperBound() ? range.upperEndpoint() : null; + + if (anyEndpoint == null) { + throw new HibernateException( + new IllegalArgumentException("The range " + range + " doesn't have any upper or lower bound!") + ); + } + + clazz = anyEndpoint.getClass(); + } + + if (clazz.equals(Integer.class)) { + return "int4range"; + } else if (clazz.equals(Long.class)) { + return "int8range"; + } else if (clazz.equals(BigDecimal.class)) { + return "numrange"; + } else if (clazz.equals(LocalDateTime.class)) { + return "tsrange"; + } else if (clazz.equals(ZonedDateTime.class) || clazz.equals(OffsetDateTime.class)) { + return "tstzrange"; + } else if (clazz.equals(LocalDate.class)) { + return "daterange"; + } + + throw new HibernateException( + new IllegalStateException("The class [" + clazz + "] is not supported!") + ); + } + + public static > Range ofString(String str, Function converter, Class clazz) { + if ("empty".equals(str)) { + if (clazz.equals(Integer.class)) { + return (Range) EMPTY_INT_RANGE; + } else if (clazz.equals(Long.class)) { + return (Range) EMPTY_LONG_RANGE; + } else if (clazz.equals(BigDecimal.class)) { + return (Range) EMPTY_BIGDECIMAL_RANGE; + } else if (clazz.equals(LocalDateTime.class)) { + return (Range) EMPTY_LOCALDATETIME_RANGE; + } else if (clazz.equals(ZonedDateTime.class)) { + return (Range) EMPTY_ZONEDDATETIME_RANGE; + } else if (clazz.equals(OffsetDateTime.class)) { + return (Range) EMPTY_OFFSETDATETIME_RANGE; + } else if (clazz.equals(LocalDate.class)) { + return (Range) EMPTY_DATE_RANGE; + } + + throw new HibernateException( + new IllegalStateException("The class [" + clazz.getName() + "] is not supported!") + ); + } + + BoundType lowerBound = str.charAt(0) == '[' ? BoundType.CLOSED : BoundType.OPEN; + BoundType upperBound = str.charAt(str.length() - 1) == ']' ? BoundType.CLOSED : BoundType.OPEN; + + int delim = str.indexOf(','); + + if (delim == -1) { + throw new HibernateException( + new IllegalArgumentException("Cannot find comma character") + ); + } + + String lowerStr = str.substring(1, delim); + String upperStr = str.substring(delim + 1, str.length() - 1); + + T lower = null; + T upper = null; + + if (lowerStr.length() > 0) { + lower = converter.apply(lowerStr); + } + + if (upperStr.length() > 0) { + upper = converter.apply(upperStr); + } + + if (lower == null && upper == null && upperBound == BoundType.OPEN && lowerBound == BoundType.OPEN) { + return Range.all(); + } + + if (lowerStr.length() == 0) { + return upperBound == BoundType.CLOSED ? + Range.atMost(upper) : + Range.lessThan(upper); + } else if (upperStr.length() == 0) { + return lowerBound == BoundType.CLOSED ? + Range.atLeast(lower) : + Range.greaterThan(lower); + } else { + return Range.range(lower, lowerBound, upper, upperBound); + } + } + + /** + * Creates the {@code BigDecimal} range from provided string: + *
{@code
+     *     Range closed = Range.bigDecimalRange("[0.1,1.1]");
+     *     Range halfOpen = Range.bigDecimalRange("(0.1,1.1]");
+     *     Range open = Range.bigDecimalRange("(0.1,1.1)");
+     *     Range leftUnbounded = Range.bigDecimalRange("(,1.1)");
+     * }
+ * + * @param range The range string, for example {@literal "[5.5,7.8]"}. + * + * @return The range of {@code BigDecimal}s. + * + * @throws NumberFormatException when one of the bounds are invalid. + */ + public static Range bigDecimalRange(String range) { + return ofString(range, BigDecimal::new, BigDecimal.class); + } + + /** + * Creates the {@code Integer} range from provided string: + *
{@code
+     *     Range closed = Range.integerRange("[1,5]");
+     *     Range halfOpen = Range.integerRange("(-1,1]");
+     *     Range open = Range.integerRange("(1,2)");
+     *     Range leftUnbounded = Range.integerRange("(,10)");
+     *     Range unbounded = Range.integerRange("(,)");
+     * }
+ * + * @param range The range string, for example {@literal "[5,7]"}. + * + * @return The range of {@code Integer}s. + * + * @throws NumberFormatException when one of the bounds are invalid. + */ + public static Range integerRange(String range) { + return ofString(range, Integer::parseInt, Integer.class); + } + + /** + * Creates the {@code Long} range from provided string: + *
{@code
+     *     Range closed = Range.longRange("[1,5]");
+     *     Range halfOpen = Range.longRange("(-1,1]");
+     *     Range open = Range.longRange("(1,2)");
+     *     Range leftUnbounded = Range.longRange("(,10)");
+     *     Range unbounded = Range.longRange("(,)");
+     * }
+ * + * @param range The range string, for example {@literal "[5,7]"}. + * + * @return The range of {@code Long}s. + * + * @throws NumberFormatException when one of the bounds are invalid. + */ + public static Range longRange(String range) { + return ofString(range, Long::parseLong, Long.class); + } + + /** + * Creates the {@code LocalDateTime} range from provided string: + *
{@code
+     *     Range closed = Range.localDateTimeRange("[2014-04-28 16:00:49,2015-04-28 16:00:49]");
+     *     Range quoted = Range.localDateTimeRange("[\"2014-04-28 16:00:49\",\"2015-04-28 16:00:49\"]");
+     *     Range iso = Range.localDateTimeRange("[\"2014-04-28T16:00:49.2358\",\"2015-04-28T16:00:49\"]");
+     * }
+ *

+ * The valid formats for bounds are: + *

    + *
  • yyyy-MM-dd HH:mm:ss[.SSSSSS]
  • + *
  • yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]
  • + *
+ * + * @param range The range string, for example {@literal "[2014-04-28 16:00:49,2015-04-28 16:00:49]"}. + * + * @return The range of {@code LocalDateTime}s. + * + * @throws DateTimeParseException when one of the bounds are invalid. + */ + public static Range localDateTimeRange(String range) { + return ofString(range, parseLocalDateTime().compose(unquote()), LocalDateTime.class); + } + + /** + * Creates the {@code LocalDate} range from provided string: + *
{@code
+     *     Range closed = Range.localDateRange("[2014-04-28,2015-04-289]");
+     *     Range quoted = Range.localDateRange("[\"2014-04-28\",\"2015-04-28\"]");
+     *     Range iso = Range.localDateRange("[\"2014-04-28\",\"2015-04-28\"]");
+     * }
+ *

+ * The valid formats for bounds are: + *

    + *
  • yyyy-MM-dd
  • + *
  • yyyy-MM-dd
  • + *
+ * + * @param range The range string, for example {@literal "[2014-04-28,2015-04-28]"}. + * + * @return The range of {@code LocalDate}s. + * + * @throws DateTimeParseException when one of the bounds are invalid. + */ + public static Range localDateRange(String range) { + Function parseLocalDate = LocalDate::parse; + return ofString(range, parseLocalDate.compose(unquote()), LocalDate.class); + } + + /** + * Creates the {@code ZonedDateTime} range from provided string: + *
{@code
+     *     Range closed = Range.zonedDateTimeRange("[2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00]");
+     *     Range quoted = Range.zonedDateTimeRange("[\"2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00\"]");
+     *     Range iso = Range.zonedDateTimeRange("[2011-12-03T10:15:30+01:00[Europe/Paris], 2012-12-03T10:15:30+01:00[Europe/Paris]]");
+     * }
+ *

+ * The valid formats for bounds are: + *

    + *
  • yyyy-MM-dd HH:mm:ss[.SSSSSS]X
  • + *
  • yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]X
  • + *
+ * + * @param rangeStr The range string, for example {@literal "[2011-12-03T10:15:30+01:00,2012-12-03T10:15:30+01:00]"}. + * + * @return The range of {@code ZonedDateTime}s. + * + * @throws DateTimeParseException when one of the bounds are invalid. + * @throws IllegalArgumentException when bounds time zones are different. + */ + public static Range zonedDateTimeRange(String rangeStr) { + Range range = ofString(rangeStr, parseZonedDateTime().compose(unquote()), ZonedDateTime.class); + if (range.hasLowerBound() && range.hasUpperBound()) { + ZoneId lowerZone = range.lowerEndpoint().getZone(); + ZoneId upperZone = range.upperEndpoint().getZone(); + if (!lowerZone.equals(upperZone)) { + Duration lowerDst = ZoneId.systemDefault().getRules().getDaylightSavings(range.lowerEndpoint().toInstant()); + Duration upperDst = ZoneId.systemDefault().getRules().getDaylightSavings(range.upperEndpoint().toInstant()); + long dstSeconds = upperDst.minus(lowerDst).getSeconds(); + if (dstSeconds < 0) { + dstSeconds *= -1; + } + long zoneDriftSeconds = ((ZoneOffset) lowerZone).getTotalSeconds() - ((ZoneOffset) upperZone).getTotalSeconds(); + if (zoneDriftSeconds < 0) { + zoneDriftSeconds *= -1; + } + + if (dstSeconds != zoneDriftSeconds) { + throw new HibernateException( + new IllegalArgumentException("The upper and lower bounds must be in same time zone!") + ); + } + } + } + return range; + } + + /** + * Creates the {@code OffsetDateTime} range from provided string: + *
{@code
+     *     Range closed = Range.offsetDateTimeRange("[2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00]");
+     *     Range quoted = Range.offsetDateTimeRange("[\"2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00\"]");
+     *     Range iso = Range.offsetDateTimeRange("[2011-12-03T10:15:30+01:00[Europe/Paris], 2012-12-03T10:15:30+01:00[Europe/Paris]]");
+     * }
+ *

+ * The valid formats for bounds are: + *

    + *
  • yyyy-MM-dd HH:mm:ss[.SSSSSS]X
  • + *
  • yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]X
  • + *
+ * + * @param rangeStr The range string, for example {@literal "[2011-12-03T10:15:30+01:00,2012-12-03T10:15:30+01:00]"}. + * + * @return The range of {@code OffsetDateTime}s. + * + * @throws DateTimeParseException when one of the bounds are invalid. + * @throws IllegalArgumentException when bounds time zones are different. + */ + public static Range offsetDateTimeRange(String rangeStr) { + return ofString(rangeStr, parseOffsetDateTime().compose(unquote()), OffsetDateTime.class); + } + + private static Function parseLocalDateTime() { + return str -> { + try { + return LocalDateTime.parse(str, LOCAL_DATE_TIME); + } catch (DateTimeParseException e) { + return LocalDateTime.parse(str); + } + }; + } + + private static Function parseZonedDateTime() { + return s -> { + try { + return ZonedDateTime.parse(s, OFFSET_DATE_TIME); + } catch (DateTimeParseException e) { + return ZonedDateTime.parse(s); + } + }; + } + + private static Function parseOffsetDateTime() { + return s -> { + try { + return OffsetDateTime.parse(s, OFFSET_DATE_TIME); + } catch (DateTimeParseException e) { + return OffsetDateTime.parse(s); + } + }; + } + + private static Function unquote() { + return s -> { + if (s.charAt(0) == '\"' && s.charAt(s.length() - 1) == '\"') { + return s.substring(1, s.length() - 1); + } + + return s; + }; + } + + public String asString(Range range) { + if (range.isEmpty()) { + return "empty"; + } + + StringBuilder sb = new StringBuilder(); + + sb.append(range.hasLowerBound() && range.lowerBoundType() == BoundType.CLOSED ? '[' : '(') + .append(range.hasLowerBound() ? asString(range.lowerEndpoint()) : "") + .append(",") + .append(range.hasUpperBound() ? asString(range.upperEndpoint()) : "") + .append(range.hasUpperBound() && range.upperBoundType() == BoundType.CLOSED ? ']' : ')'); + + return sb.toString(); + } + + private String asString(Object value) { + if (value instanceof ZonedDateTime) { + return OFFSET_DATE_TIME.format((ZonedDateTime) value); + } + return value.toString(); + } + + @Override + public void setParameterValues(Properties parameters) { + final XProperty xProperty = (XProperty) parameters.get(DynamicParameterizedType.XPROPERTY); + if (xProperty instanceof JavaXMember) { + type = ReflectionUtils.invokeGetter(xProperty, "javaType"); + } else { + type = ((ParameterType) parameters.get(PARAMETER_TYPE)).getReturnedClass(); + } + + if (type instanceof ParameterizedType) { + elementType = (Class) ((ParameterizedType) type).getActualTypeArguments()[0]; + } + } + + public Class getElementType() { + return elementType; + } + +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/search/PostgreSQLTSVectorType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/search/PostgreSQLTSVectorType.java new file mode 100644 index 000000000..e8236988c --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/search/PostgreSQLTSVectorType.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hibernate.type.search; + +import com.vladmihalcea.hibernate.type.search.internal.PostgreSQLTSVectorSqlTypeDescriptor; +import com.vladmihalcea.hibernate.type.search.internal.PostgreSQLTSVectorTypeDescriptor; +import com.vladmihalcea.hibernate.type.util.Configuration; +import org.hibernate.type.AbstractSingleColumnStandardBasicType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.util.Properties; + +/** + * Maps a {@link String} object type to a PostgreSQL TSVector column type. + * + * @author Vlad Mihalcea + * @author Philip Riecks + */ +public class PostgreSQLTSVectorType + extends AbstractSingleColumnStandardBasicType implements DynamicParameterizedType { + + public static final PostgreSQLTSVectorType INSTANCE = new PostgreSQLTSVectorType(); + + private Configuration configuration; + + public PostgreSQLTSVectorType() { + super(PostgreSQLTSVectorSqlTypeDescriptor.INSTANCE, new PostgreSQLTSVectorTypeDescriptor()); + } + + public PostgreSQLTSVectorType(Configuration configuration) { + this(); + this.configuration = configuration; + } + + public PostgreSQLTSVectorType(org.hibernate.type.spi.TypeBootstrapContext typeBootstrapContext) { + this(new Configuration(typeBootstrapContext.getConfigurationSettings())); + } + + @Override + public String getName() { + return "tsvector"; + } + + @Override + public void setParameterValues(Properties parameters) { + ((PostgreSQLTSVectorTypeDescriptor) getJavaTypeDescriptor()).setParameterValues(parameters); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/search/internal/PostgreSQLTSVectorSqlTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/search/internal/PostgreSQLTSVectorSqlTypeDescriptor.java new file mode 100644 index 000000000..0996936f3 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/search/internal/PostgreSQLTSVectorSqlTypeDescriptor.java @@ -0,0 +1,65 @@ +package com.vladmihalcea.hibernate.type.search.internal; + +import com.vladmihalcea.hibernate.util.ReflectionUtils; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcType; + +import java.sql.*; + +public class PostgreSQLTSVectorSqlTypeDescriptor implements JdbcType { + + public static final PostgreSQLTSVectorSqlTypeDescriptor INSTANCE = new PostgreSQLTSVectorSqlTypeDescriptor(); + + @Override + public int getJdbcTypeCode() { + return Types.OTHER; + } + + @Override + public ValueBinder getBinder(final JavaType JavaType) { + return new BasicBinder(JavaType, this) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + Object holder = ReflectionUtils.newInstance("org.postgresql.util.PGobject"); + ReflectionUtils.invokeSetter(holder, "type", "tsvector"); + ReflectionUtils.invokeSetter(holder, "value", JavaType.unwrap(value, String.class, options)); + st.setObject(index, holder); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + Object holder = ReflectionUtils.newInstance("org.postgresql.util.PGobject"); + ReflectionUtils.invokeSetter(holder, "type", "tsvector"); + ReflectionUtils.invokeSetter(holder, "value", JavaType.unwrap(value, String.class, options)); + + st.setObject(name, holder); + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType JavaType) { + return new BasicExtractor(JavaType, this) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return JavaType.wrap(rs.getString(paramIndex), options); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return JavaType.wrap(statement.getString(index), options); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return JavaType.wrap(statement.getString(name), options); + } + }; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/search/internal/PostgreSQLTSVectorTypeDescriptor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/search/internal/PostgreSQLTSVectorTypeDescriptor.java new file mode 100644 index 000000000..fd0fbe272 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/search/internal/PostgreSQLTSVectorTypeDescriptor.java @@ -0,0 +1,89 @@ +package com.vladmihalcea.hibernate.type.search.internal; + +import com.vladmihalcea.hibernate.util.ReflectionUtils; +import org.hibernate.annotations.common.reflection.XProperty; +import org.hibernate.annotations.common.reflection.java.JavaXMember; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; +import org.hibernate.usertype.DynamicParameterizedType; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Properties; + +public class PostgreSQLTSVectorTypeDescriptor extends AbstractClassJavaType implements DynamicParameterizedType { + + public PostgreSQLTSVectorTypeDescriptor() { + super(Object.class); + } + + private Type type; + + @Override + public void setParameterValues(Properties parameters) { + final XProperty xProperty = (XProperty) parameters.get(DynamicParameterizedType.XPROPERTY); + if (xProperty instanceof JavaXMember) { + type = ReflectionUtils.invokeGetter(xProperty, "javaType"); + } else { + type = ((ParameterType) parameters.get(PARAMETER_TYPE)).getReturnedClass(); + } + } + + @Override + public boolean areEqual(Object one, Object another) { + if (one == another) { + return true; + } + if (one == null || another == null) { + return false; + } + if (one instanceof String && another instanceof String) { + return one.equals(another); + } + return one.equals(another); + } + + @Override + public String toString(Object value) { + return value.toString(); + } + + @Override + public Object fromString(CharSequence string) { + if (String.class.isAssignableFrom(typeToClass())) { + return string; + } + return string; + } + + @SuppressWarnings({"unchecked"}) + @Override + public X unwrap(Object value, Class type, WrapperOptions options) { + if (value == null) { + return null; + } + if (String.class.isAssignableFrom(type)) { + return (X) toString(value); + } + throw unknownUnwrap(type); + } + + @Override + public Object wrap(X value, WrapperOptions options) { + if (value == null) { + return null; + } + return fromString(value.toString()); + } + + private Class typeToClass() { + Type classType = type; + if (type instanceof ParameterizedType) { + classType = ((ParameterizedType) type).getRawType(); + } else if (type instanceof TypeVariable) { + classType = ((TypeVariable) type).getGenericDeclaration().getClass(); + } + return (Class) classType; + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/CamelCaseToSnakeCaseNamingStrategy.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/CamelCaseToSnakeCaseNamingStrategy.java new file mode 100644 index 000000000..78510c345 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/CamelCaseToSnakeCaseNamingStrategy.java @@ -0,0 +1,21 @@ +package com.vladmihalcea.hibernate.type.util; + +/** + * Maps the JPA camelCase properties to snake_case database identifiers. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @deprecated use {@link com.vladmihalcea.hibernate.naming.CamelCaseToSnakeCaseNamingStrategy} instead + * + * @author Vlad Mihalcea + */ +@Deprecated +public class CamelCaseToSnakeCaseNamingStrategy extends com.vladmihalcea.hibernate.naming.CamelCaseToSnakeCaseNamingStrategy { + + public static final CamelCaseToSnakeCaseNamingStrategy INSTANCE = new CamelCaseToSnakeCaseNamingStrategy(); + + public static final String CAMEL_CASE_REGEX = "([a-z]+)([A-Z]+)"; + + public static final String SNAKE_CASE_PATTERN = "$1\\_$2"; + +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ClassImportIntegrator.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ClassImportIntegrator.java new file mode 100644 index 000000000..4808113bc --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ClassImportIntegrator.java @@ -0,0 +1,89 @@ +package com.vladmihalcea.hibernate.type.util; + +import org.hibernate.boot.Metadata; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +import java.util.List; + +/** + * The {@link ClassImportIntegrator} implements the Hibernate {@link Integrator} contract + * and allows you to provide a {@link List} of classes to be imported using their simple name. + * + * For instance, you could use a DTO simple class name, instead of the fully-qualified name + * when building a constructor expression in a JPQL query. + * + * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @author Vlad Mihalcea + */ +public class ClassImportIntegrator implements Integrator { + + private static final String DOT = "."; + + private final List classImportList; + + private String excludedPath; + + /** + * Builds a new integrator that can register the provided classes. + * + * @param classImportList list of classes to be imported + */ + public ClassImportIntegrator(List classImportList) { + this.classImportList = classImportList; + } + + /** + * Exclude the provided parent path and register the remaining relative path. + * If the {@link #excludedPath} is not set, then the package is excluded and + * only the simple class name is registered. + * + * For instance, if you provide the {@code com.vladmihalcea.hibernate.type} path, + * and register a class whose fully-qualified name is {@code com.vladmihalcea.hibernate.type.json.PostDTO}, + * then the class is going to be registered under the {@code json.PostDTO} alias. + * + * @param path path to be excluded. + * @return the {@link ClassImportIntegrator} object reference + */ + public ClassImportIntegrator excludePath(String path) { + this.excludedPath = path.endsWith(DOT) ? path : path + DOT; + return this; + } + + /** + * Register the provided classes by their simple name or relative package and class name. + * + * @param metadata metadata + * @param sessionFactory Hibernate session factory + * @param serviceRegistry Hibernate service registry + */ + @Override + public void integrate( + Metadata metadata, + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + for(Class classImport : classImportList) { + String key; + if(excludedPath != null) { + key = classImport.getName().replace(excludedPath, ""); + } else { + key = classImport.getSimpleName(); + } + + metadata.getImports().put( + key, + classImport.getName() + ); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) {} +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/Configuration.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/Configuration.java new file mode 100644 index 000000000..d668bc969 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/Configuration.java @@ -0,0 +1,378 @@ +package com.vladmihalcea.hibernate.type.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vladmihalcea.hibernate.util.ClassLoaderUtils; +import com.vladmihalcea.hibernate.util.ReflectionUtils; +import com.vladmihalcea.hibernate.util.StringUtils; +import org.hibernate.cfg.Environment; + +import jakarta.persistence.EntityManagerFactory; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.Properties; +import java.util.function.Supplier; + +import static com.vladmihalcea.hibernate.util.LogUtils.LOGGER; + +/** + * Configuration - It allows declarative configuration through the hibernate.properties file + * or the hibernate-types.properties file. + * + * The properties from hibernate-types.properties can override the ones from the hibernate.properties file. + * + * It loads the {@link Properties} configuration file and makes them available to other components. + * + * @author Vlad Mihalcea + * @since 2.1.0 + */ +public class Configuration { + + public static final Configuration INSTANCE = new Configuration(); + + public static final String PROPERTIES_FILE_PATH = "hibernate-types.properties.path"; + public static final String PROPERTIES_FILE_NAME = "hibernate-types.properties"; + public static final String APPLICATION_PROPERTIES_FILE_NAME = "application.properties"; + + /** + * Each Property has a well-defined key. + */ + public enum PropertyKey { + JACKSON_OBJECT_MAPPER("hibernate.types.jackson.object.mapper"), + JSON_SERIALIZER("hibernate.types.json.serializer"), + PRINT_BANNER("hibernate.types.print.banner"); + + private final String key; + + PropertyKey(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + } + + private final Properties properties = Environment.getProperties(); + + private Configuration() { + load(); + } + + public Configuration(Map settings) { + load(); + properties.putAll(settings); + + this.printBanner(); + } + + /** + * Load {@link Properties} from the resolved {@link InputStream} + */ + private void load() { + String[] propertiesFilePaths = new String[] { + APPLICATION_PROPERTIES_FILE_NAME, + PROPERTIES_FILE_NAME, + System.getProperty(PROPERTIES_FILE_NAME), + + }; + + for (String propertiesFilePath : propertiesFilePaths) { + if (propertiesFilePath != null) { + InputStream propertiesInputStream = null; + try { + propertiesInputStream = propertiesInputStream(propertiesFilePath); + if (propertiesInputStream != null) { + properties.load(propertiesInputStream); + } + } catch (IOException e) { + LOGGER.error("Can't load properties", e); + } finally { + try { + if (propertiesInputStream != null) { + propertiesInputStream.close(); + } + } catch (IOException e) { + LOGGER.error("Can't close the properties InputStream", e); + } + } + } + } + } + + /** + * Get {@link Properties} file {@link InputStream} + * + * @param propertiesFilePath properties file path + * @return {@link Properties} file {@link InputStream} + * @throws IOException the file couldn't be loaded properly + */ + private InputStream propertiesInputStream(String propertiesFilePath) throws IOException { + if (propertiesFilePath != null) { + URL propertiesFileUrl; + try { + propertiesFileUrl = new URL(propertiesFilePath); + } catch (MalformedURLException ignore) { + propertiesFileUrl = ClassLoaderUtils.getResource(propertiesFilePath); + if (propertiesFileUrl == null) { + File f = new File(propertiesFilePath); + if (f.exists() && f.isFile()) { + try { + propertiesFileUrl = f.toURI().toURL(); + } catch (MalformedURLException e) { + LOGGER.error( + "The property " + propertiesFilePath + " can't be resolved to either a URL, " + + "a classpath resource or a File" + ); + } + } + } + } + if (propertiesFileUrl != null) { + return propertiesFileUrl.openStream(); + } + } + return ClassLoaderUtils.getResourceAsStream(propertiesFilePath); + } + + /** + * Get all properties. + * + * @return properties. + */ + public Properties getProperties() { + return properties; + } + + /** + * Get {@link ObjectMapperWrapper} reference + * + * @return {@link ObjectMapperWrapper} reference + */ + public ObjectMapperWrapper getObjectMapperWrapper() { + Object objectMapperPropertyInstance = instantiateClass(PropertyKey.JACKSON_OBJECT_MAPPER); + + ObjectMapperWrapper objectMapperWrapper = new ObjectMapperWrapper(); + + if (objectMapperPropertyInstance != null) { + if(objectMapperPropertyInstance instanceof ObjectMapperSupplier) { + ObjectMapper objectMapper = ((ObjectMapperSupplier) objectMapperPropertyInstance).get(); + if(objectMapper != null) { + objectMapperWrapper = new ObjectMapperWrapper(objectMapper); + } + } + else if (objectMapperPropertyInstance instanceof Supplier) { + Supplier objectMapperSupplier = (Supplier) objectMapperPropertyInstance; + objectMapperWrapper = new ObjectMapperWrapper(objectMapperSupplier.get()); + } + else if (objectMapperPropertyInstance instanceof ObjectMapper) { + ObjectMapper objectMapper = (ObjectMapper) objectMapperPropertyInstance; + objectMapperWrapper = new ObjectMapperWrapper(objectMapper); + } + } + + Object jsonSerializerPropertyInstance = instantiateClass(PropertyKey.JSON_SERIALIZER); + + if (jsonSerializerPropertyInstance != null) { + JsonSerializer jsonSerializer = null; + + if(jsonSerializerPropertyInstance instanceof JsonSerializerSupplier) { + jsonSerializer = ((JsonSerializerSupplier) jsonSerializerPropertyInstance).get(); + } + else if (jsonSerializerPropertyInstance instanceof Supplier) { + Supplier jsonSerializerSupplier = (Supplier) jsonSerializerPropertyInstance; + jsonSerializer = jsonSerializerSupplier.get(); + } + else if (jsonSerializerPropertyInstance instanceof JsonSerializer) { + jsonSerializer = (JsonSerializer) jsonSerializerPropertyInstance; + } + + if (jsonSerializer != null) { + objectMapperWrapper.setJsonSerializer(jsonSerializer); + } + } + + return objectMapperWrapper; + } + + /** + * Get Integer property value + * + * @param propertyKey property key + * @return Integer property value + */ + public Integer integerProperty(PropertyKey propertyKey) { + Integer value = null; + String property = properties.getProperty(propertyKey.getKey()); + if (property != null) { + value = Integer.valueOf(property); + } + return value; + } + + /** + * Get Long property value + * + * @param propertyKey property key + * @return Long property value + */ + public Long longProperty(PropertyKey propertyKey) { + Long value = null; + String property = properties.getProperty(propertyKey.getKey()); + if (property != null) { + value = Long.valueOf(property); + } + return value; + } + + /** + * Get Boolean property value + * + * @param propertyKey property key + * @return Boolean property value + */ + public Boolean booleanProperty(PropertyKey propertyKey) { + Boolean value = null; + String property = properties.getProperty(propertyKey.getKey()); + if (property != null) { + value = Boolean.valueOf(property); + } + return value; + } + + /** + * Get Class property value + * + * @param propertyKey property key + * @param class generic type + * @return Class property value + */ + public Class classProperty(PropertyKey propertyKey) { + Class clazz = null; + String property = properties.getProperty(propertyKey.getKey()); + if (property != null) { + try { + return ClassLoaderUtils.loadClass(property); + } catch (ClassNotFoundException e) { + LOGGER.error("Couldn't load the " + property + " class given by the " + propertyKey + " property", e); + } + } + return clazz; + } + + /** + * Instantiate class associated to the given property key + * + * @param propertyKey property key + * @param class parameter type + * @return class instance + */ + private T instantiateClass(PropertyKey propertyKey) { + T object = null; + String property = properties.getProperty(propertyKey.getKey()); + if (property != null) { + try { + Class clazz = ClassLoaderUtils.loadClass(property); + LOGGER.debug("Instantiate {}", clazz); + object = clazz.newInstance(); + } catch (ClassNotFoundException e) { + LOGGER.error("Couldn't load the " + property + " class given by the " + propertyKey + " property", e); + } catch (InstantiationException e) { + LOGGER.error("Couldn't instantiate the " + property + " class given by the " + propertyKey + " property", e); + } catch (IllegalAccessException e) { + LOGGER.error("Couldn't access the " + property + " class given by the " + propertyKey + " property", e); + } + } + return object; + } + + /** + * Print the banner into the log. + */ + private void printBanner() { + if (this.isHypersistenceOptimizer() || bannerPrinted) { + return; + } + String printBannerValue = properties.getProperty(PropertyKey.PRINT_BANNER.getKey()); + if(printBannerValue != null && !Boolean.valueOf(printBannerValue)) { + return; + } + + LOGGER.info( + StringUtils.join( + StringUtils.LINE_SEPARATOR, + "This framework is proudly powered by:", + "", + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", + " _ _ _ _", + "| | | | (_) | |", + "| |__| |_ _ _ __ ___ _ __ ___ _ ___| |_ ___ _ __ ___ ___", + "| __ | | | | '_ \\ / _ \\ '__/ __| / __| __/ _ \\ '_ \\ / __/ _ \\", + "| | | | |_| | |_) | __/ | \\__ \\ \\__ \\ || __/ | | | (_| __/", + "|_| |_|\\__, | .__/ \\___|_| |___/_|___/\\__\\___|_| |_|\\___\\___|", + " __/ | |", + " |___/|_|", + "", + "At Hypersistence, we only build amazing tools, like Hibernate Types, Flexy Pool, or Hypersistence Optimizer.", + "", + "What if there were a tool that could automatically detect JPA and Hibernate performance issues?", + "", + "Hypersistence Optimizer is that tool! For more details, go to: ", + "", + "https://vladmihalcea.com/hypersistence-optimizer/", + "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", + "" + ) + ); + bannerPrinted = true; + } + + private boolean isHypersistenceOptimizer() { + try { + Class licenseClass = ReflectionUtils.getClassOrNull("io.hypersistence.optimizer.core.License"); + + if(licenseClass != null) { + Class jpaConfigClass = ReflectionUtils.getClassOrNull("io.hypersistence.optimizer.core.config.JpaConfig"); + Object jpaConfig = ReflectionUtils.newInstance( + jpaConfigClass, + new Object[] { + null + }, + new Class[] { + EntityManagerFactory.class + } + ); + + Object license = ReflectionUtils.newInstance( + licenseClass, + new Object[] { + jpaConfig + }, + new Class[] { + ReflectionUtils.getClassOrNull("io.hypersistence.optimizer.core.LicenseConfig") + } + ); + + Properties properties = ReflectionUtils.invokeGetter(license, "properties"); + Class propertyClass = ReflectionUtils.getClassOrNull("io.hypersistence.optimizer.core.License$Property"); + Object trialVersionKey = Enum.valueOf(propertyClass, "TRIAL_VERSION"); + Object validUntilMillisKey = Enum.valueOf(propertyClass, "VALID_UNTIL_MILLIS"); + + boolean trialVersion = Boolean.parseBoolean(properties.getProperty(ReflectionUtils.invokeGetter(trialVersionKey, "key"))); + long validUntilMillis = Long.parseLong(properties.getProperty(ReflectionUtils.invokeGetter(validUntilMillisKey, "key"))); + + if(!trialVersion && validUntilMillis > System.currentTimeMillis()) { + return true; + } + } + } catch (Exception ignore) { + } + + return false; + } + + private static boolean bannerPrinted = false; +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/JsonSerializer.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/JsonSerializer.java new file mode 100644 index 000000000..e4d38b180 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/JsonSerializer.java @@ -0,0 +1,18 @@ +package com.vladmihalcea.hibernate.type.util; + +/** + * Contract for serializing JSON objects. + * + * @author Vlad Mihalcea + */ +public interface JsonSerializer { + + /** + * Clone JSON object. + * + * @param jsonObject JSON object + * @param JSON object parameterized type + * @return cloned JSON object + */ + T clone(T jsonObject); +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/JsonSerializerSupplier.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/JsonSerializerSupplier.java new file mode 100644 index 000000000..74b644270 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/JsonSerializerSupplier.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hibernate.type.util; + +/** + * Supplies a custom reference of a Jackson {@link JsonSerializer} + * + * @author Vlad Mihalcea + * @since 2.1.0 + */ +public interface JsonSerializerSupplier { + + /** + * Get custom {@link JsonSerializer} reference + * + * @return custom {@link JsonSerializer} reference + */ + JsonSerializer get(); +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ListResultTransformer.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ListResultTransformer.java new file mode 100644 index 000000000..979e20bf4 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ListResultTransformer.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hibernate.type.util; + +import org.hibernate.transform.ResultTransformer; + +import java.util.List; + +/** + * The {@link com.vladmihalcea.hibernate.type.util.ListResultTransformer} simplifies the way + * we can use a ResultTransformer by defining a default implementation for the + * {@link ResultTransformer#transformList(List)} method. + *

+ * This way, the {@link com.vladmihalcea.hibernate.type.util.ListResultTransformer} can be used + * as a functional interface. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @deprecated use {@link com.vladmihalcea.hibernate.query.ListResultTransformer} instead + * + * @author Vlad Mihalcea + * @since 2.9.0 + */ +@Deprecated +@FunctionalInterface +public interface ListResultTransformer extends com.vladmihalcea.hibernate.query.ListResultTransformer { + +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/MapResultTransformer.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/MapResultTransformer.java new file mode 100644 index 000000000..f7c7ea93e --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/MapResultTransformer.java @@ -0,0 +1,27 @@ +package com.vladmihalcea.hibernate.type.util; + +import java.util.Map; + +/** + * The {@link com.vladmihalcea.hibernate.type.util.MapResultTransformer} allows us to return + * a {@link Map} from a JPA {@link jakarta.persistence.Query}. + *

+ * If there are aliases named as {@code key} or {@code value}, + * then those will be used. + *

+ * Otherwise, the first column value is the key while the second one is the Map value. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @deprecated use {@link com.vladmihalcea.hibernate.query.MapResultTransformer} instead + * + * @author Vlad Mihalcea + * @since 2.9.0 + */ +@Deprecated +public class MapResultTransformer extends com.vladmihalcea.hibernate.query.MapResultTransformer { + + public static final String KEY_ALIAS = "map_key"; + + public static final String VALUE_ALIAS = "map_value"; +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ObjectMapperJsonSerializer.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ObjectMapperJsonSerializer.java new file mode 100644 index 000000000..acd1e46bd --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ObjectMapperJsonSerializer.java @@ -0,0 +1,83 @@ +package com.vladmihalcea.hibernate.type.util; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.type.TypeFactory; +import org.hibernate.internal.util.SerializationHelper; +import org.hibernate.type.SerializationException; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class ObjectMapperJsonSerializer implements JsonSerializer { + + private final ObjectMapperWrapper objectMapperWrapper; + + public ObjectMapperJsonSerializer(ObjectMapperWrapper objectMapperWrapper) { + this.objectMapperWrapper = objectMapperWrapper; + } + + @Override + public T clone(T object) { + if (object instanceof JsonNode) { + return (T) ((JsonNode) object).deepCopy(); + } + + if (object instanceof Collection) { + Object firstElement = findFirstNonNullElement((Collection) object); + if (firstElement != null && !(firstElement instanceof Serializable)) { + JavaType type = TypeFactory.defaultInstance().constructParametricType(object.getClass(), firstElement.getClass()); + return objectMapperWrapper.fromBytes(objectMapperWrapper.toBytes(object), type); + } + } + + if (object instanceof Map) { + Map.Entry firstEntry = this.findFirstNonNullEntry((Map) object); + if (firstEntry != null) { + Object key = firstEntry.getKey(); + Object value = firstEntry.getValue(); + if (!(key instanceof Serializable) || !(value instanceof Serializable)) { + JavaType type = TypeFactory.defaultInstance().constructParametricType(object.getClass(), key.getClass(), value.getClass()); + return (T) objectMapperWrapper.fromBytes(objectMapperWrapper.toBytes(object), type); + } + } + } + if (object instanceof Serializable) { + try { + return (T) SerializationHelper.clone((Serializable) object); + } catch (SerializationException e) { + //it is possible that object itself implements java.io.Serializable, but underlying structure does not + //in this case we switch to the other JSON marshaling strategy which doesn't use the Java serialization + return jsonClone(object); + } + } else { + return jsonClone(object); + } + } + + private Object findFirstNonNullElement(Collection collection) { + for (java.lang.Object element : collection) { + if (element != null) { + return element; + } + } + return null; + } + + private Map.Entry findFirstNonNullEntry(Map map) { + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() != null && entry.getValue() != null) { + return entry; + } + } + return null; + } + + private T jsonClone(T object) { + return objectMapperWrapper.fromBytes(objectMapperWrapper.toBytes(object), (Class) object.getClass()); + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ObjectMapperSupplier.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ObjectMapperSupplier.java new file mode 100644 index 000000000..8ac51237d --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ObjectMapperSupplier.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hibernate.type.util; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Supplies a custom reference of a Jackson {@link ObjectMapper} + * + * @author Vlad Mihalcea + * @since 2.1.0 + */ +public interface ObjectMapperSupplier { + + /** + * Get custom {@link ObjectMapper} reference + * + * @return custom {@link ObjectMapper} reference + */ + ObjectMapper get(); +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ObjectMapperWrapper.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ObjectMapperWrapper.java new file mode 100644 index 000000000..6a57d729b --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ObjectMapperWrapper.java @@ -0,0 +1,164 @@ +package com.vladmihalcea.hibernate.type.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.hibernate.HibernateException; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.time.OffsetDateTime; + +import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; + +/** + * Wraps a Jackson {@link ObjectMapper} so that you can supply your own {@link ObjectMapper} reference. + * + * @author Vlad Mihalcea + * @since 2.1.0 + */ +public class ObjectMapperWrapper { + + public static final ObjectMapperWrapper INSTANCE = new ObjectMapperWrapper(); + + private final ObjectMapper objectMapper; + + private JsonSerializer jsonSerializer; + + public ObjectMapperWrapper() { + this(new ObjectMapper() + .findAndRegisterModules() + .registerModule( + new SimpleModule() + .addSerializer(OffsetDateTime.class, OffsetDateTimeSerializer.INSTANCE) + .addDeserializer(OffsetDateTime.class, OffsetDateTimeDeserializer.INSTANCE) + ) + ); + } + + public ObjectMapperWrapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + this.jsonSerializer = new ObjectMapperJsonSerializer(this); + } + + public void setJsonSerializer(JsonSerializer jsonSerializer) { + this.jsonSerializer = jsonSerializer; + } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + public T fromString(String string, Class clazz) { + try { + return objectMapper.readValue(string, clazz); + } catch (IOException e) { + throw new HibernateException( + new IllegalArgumentException("The given string value: " + string + " cannot be transformed to Json object", e) + ); + } + } + + public T fromString(String string, Type type) { + try { + return objectMapper.readValue(string, objectMapper.getTypeFactory().constructType(type)); + } catch (IOException e) { + throw new HibernateException( + new IllegalArgumentException("The given string value: " + string + " cannot be transformed to Json object", e) + ); + } + } + + public T fromBytes(byte[] value, Class clazz) { + try { + return objectMapper.readValue(value, clazz); + } catch (IOException e) { + throw new HibernateException( + new IllegalArgumentException("The given byte array cannot be transformed to Json object", e) + ); + } + } + + public T fromBytes(byte[] value, Type type) { + try { + return objectMapper.readValue(value, objectMapper.getTypeFactory().constructType(type)); + } catch (IOException e) { + throw new HibernateException( + new IllegalArgumentException("The given byte array cannot be transformed to Json object", e) + ); + } + } + + public String toString(Object value) { + try { + return objectMapper.writeValueAsString(value); + } catch (JsonProcessingException e) { + throw new HibernateException( + new IllegalArgumentException("The given Json object value: " + value + " cannot be transformed to a String", e) + ); + } + } + + public byte[] toBytes(Object value) { + try { + return objectMapper.writeValueAsBytes(value); + } catch (JsonProcessingException e) { + throw new HibernateException( + new IllegalArgumentException("The given Json object value: " + value + " cannot be transformed to a byte array", e) + ); + } + } + + public JsonNode toJsonNode(String value) { + try { + return objectMapper.readTree(value); + } catch (IOException e) { + throw new HibernateException( + new IllegalArgumentException(e) + ); + } + } + + public T clone(T value) { + return jsonSerializer.clone(value); + } + + public static class OffsetDateTimeSerializer extends com.fasterxml.jackson.databind.JsonSerializer { + + public static final OffsetDateTimeSerializer INSTANCE = new OffsetDateTimeSerializer(); + + @Override + public void serialize(OffsetDateTime offsetDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + if (offsetDateTime == null) { + jsonGenerator.writeNull(); + } else { + jsonGenerator.writeString(offsetDateTime.format(ISO_OFFSET_DATE_TIME)); + } + } + + @Override + public Class handledType() { + return OffsetDateTime.class; + } + } + + public static class OffsetDateTimeDeserializer extends JsonDeserializer { + + public static final OffsetDateTimeDeserializer INSTANCE = new OffsetDateTimeDeserializer(); + + @Override + public OffsetDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + if (jsonParser.getText() != null) { + return OffsetDateTime.parse(jsonParser.getText(), ISO_OFFSET_DATE_TIME); + } + return null; + } + + @Override + public Class handledType() { + return OffsetDateTime.class; + } + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterizedParameterType.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterizedParameterType.java new file mode 100644 index 000000000..b29885cec --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/ParameterizedParameterType.java @@ -0,0 +1,60 @@ +package com.vladmihalcea.hibernate.type.util; + +import org.hibernate.usertype.DynamicParameterizedType; + +import java.lang.annotation.Annotation; + +/** + * A stub {@code ParameterType} that returns sane values for {@link #getReturnedClass()} and + * {@link #getAnnotationsMethod()}. + * + * @author Jan-Willem Gmelig Meyling + */ +public class ParameterizedParameterType implements DynamicParameterizedType.ParameterType { + + private final Class clasz; + + public ParameterizedParameterType(Class clasz) { + this.clasz = clasz; + } + + @Override + public Class getReturnedClass() { + return clasz; + } + + @Override + public Annotation[] getAnnotationsMethod() { + return new Annotation[0]; + } + + @Override + public String getCatalog() { + throw new UnsupportedOperationException(); + } + + @Override + public String getSchema() { + throw new UnsupportedOperationException(); + } + + @Override + public String getTable() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPrimaryKey() { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getColumns() { + throw new UnsupportedOperationException(); + } + + @Override + public Long[] getColumnLengths() { + throw new UnsupportedOperationException(); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/SQLExtractor.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/SQLExtractor.java new file mode 100644 index 000000000..331c73e54 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/type/util/SQLExtractor.java @@ -0,0 +1,20 @@ +package com.vladmihalcea.hibernate.type.util; + +/** + * The {@link com.vladmihalcea.hibernate.query.SQLExtractor} allows you to extract the + * underlying SQL query generated by a JPQL or JPA Criteria API query. + *

+ * For more details about how to use it, check out this article on vladmihalcea.com. + * + * @deprecated use {@link com.vladmihalcea.hibernate.query.SQLExtractor} instead + * + * @author Vlad Mihalcea + * @since 2.9.11 + */ +@Deprecated +public class SQLExtractor extends com.vladmihalcea.hibernate.query.SQLExtractor { + + private SQLExtractor() { + throw new UnsupportedOperationException("SQLExtractor is not instantiable!"); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/ClassLoaderUtils.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/ClassLoaderUtils.java new file mode 100644 index 000000000..35575039b --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/ClassLoaderUtils.java @@ -0,0 +1,77 @@ +package com.vladmihalcea.hibernate.util; + +import java.io.InputStream; +import java.net.URL; + +/** + * ClassLoaderUtils - Class loading related utilities holder. + * + * @author Vlad Mihalcea + * @since 2.1.0 + */ +public final class ClassLoaderUtils { + + private ClassLoaderUtils() { + throw new UnsupportedOperationException("ClassLoaderUtils is not instantiable!"); + } + + /** + * Load the available ClassLoader + * + * @return ClassLoader + */ + public static ClassLoader getClassLoader() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + return (classLoader != null) ? classLoader : ClassLoaderUtils.class.getClassLoader(); + } + + /** + * Load the Class denoted by the given string representation + * + * @param className class string representation + * @param class generic type + * @return Class + * @throws ClassNotFoundException if the class cannot be resolved + */ + @SuppressWarnings("unchecked") + public static Class loadClass(String className) throws ClassNotFoundException { + return (Class) getClassLoader().loadClass(className); + } + + /** + * Find if Class denoted by the given string representation is loadable + * + * @param className class string representation + * @return Class + */ + @SuppressWarnings("unchecked") + public static boolean findClass(String className) { + try { + return getClassLoader().loadClass(className) != null; + } catch (ClassNotFoundException e) { + return false; + } catch (NoClassDefFoundError e) { + return false; + } + } + + /** + * Get the resource URL + * + * @param resourceName resource name + * @return resource URL + */ + public static URL getResource(String resourceName) { + return getClassLoader().getResource(resourceName); + } + + /** + * Get the resource InputStream + * + * @param resourceName resource name + * @return resource InputStream + */ + public static InputStream getResourceAsStream(String resourceName) { + return getClassLoader().getResourceAsStream(resourceName); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/LogUtils.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/LogUtils.java new file mode 100644 index 000000000..18025452a --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/LogUtils.java @@ -0,0 +1,15 @@ +package com.vladmihalcea.hibernate.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link LogUtils} class unifies the framework logging capabilities. + * + * @author Vlad Mihalcea + * @since 2.9.5 + */ +public class LogUtils { + + public static final Logger LOGGER = LoggerFactory.getLogger("Hibernate Types"); +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/ReflectionUtils.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/ReflectionUtils.java new file mode 100644 index 000000000..2b487721a --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/ReflectionUtils.java @@ -0,0 +1,659 @@ +package com.vladmihalcea.hibernate.util; + +import java.lang.reflect.*; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * ReflectionUtils - Reflection utilities holder. + * + * @author Vlad Mihalcea + * @since 1.0 + */ +public final class ReflectionUtils { + + private static final String GETTER_PREFIX = "get"; + + private static final String SETTER_PREFIX = "set"; + + /** + * Prevent any instantiation. + */ + private ReflectionUtils() { + throw new UnsupportedOperationException("The " + getClass() + " is not instantiable!"); + } + + /** + * Instantiate a new {@link Object} of the provided type. + * + * @param className The fully-qualified Java class name of the {@link Object} to instantiate + * @param class type + * @return new Java {@link Object} of the provided type + */ + public static T newInstance(String className) { + Class clazz = getClass(className); + return newInstance(clazz); + } + + /** + * Instantiate a new {@link Object} of the provided type. + * + * @param clazz The Java {@link Class} of the {@link Object} to instantiate + * @param class type + * @return new Java {@link Object} of the provided type + */ + @SuppressWarnings("unchecked") + public static T newInstance(Class clazz) { + try { + return (T) clazz.newInstance(); + } catch (InstantiationException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Instantiate a new {@link Object} of the provided type. + * + * @param clazz The Java {@link Class} of the {@link Object} to instantiate + * @param args The arguments that need to be passed to the constructor + * @param argsTypes The argument types that need to be passed to the constructor + * @param class type + * @return new Java {@link Object} of the provided type + */ + @SuppressWarnings("unchecked") + public static T newInstance(Class clazz, Object[] args, Class[] argsTypes) { + try { + Constructor constructor = clazz.getDeclaredConstructor(argsTypes); + constructor.setAccessible(true); + return constructor.newInstance(args); + } catch (InstantiationException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (NoSuchMethodException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Get the {@link Field} with the given name belonging to the provided Java {@link Class}. + * + * @param targetClass the provided Java {@link Class} the field belongs to + * @param fieldName the {@link Field} name + * @return the {@link Field} matching the given name + */ + public static Field getField(Class targetClass, String fieldName) { + Field field = null; + + try { + field = targetClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + try { + field = targetClass.getField(fieldName); + } catch (NoSuchFieldException ignore) { + } + + if (!targetClass.getSuperclass().equals(Object.class)) { + return getField(targetClass.getSuperclass(), fieldName); + } else { + throw handleException(e); + } + } finally { + if (field != null) { + field.setAccessible(true); + } + } + + return field; + } + + /** + * Get the {@link Field} with the given name belonging to the provided Java {@link Class} or {@code null} + * if no {@link Field} was found. + * + * @param targetClass the provided Java {@link Class} the field belongs to + * @param fieldName the {@link Field} name + * @return the {@link Field} matching the given name or {@code null} + */ + public static Field getFieldOrNull(Class targetClass, String fieldName) { + try { + return getField(targetClass, fieldName); + } catch (IllegalArgumentException e) { + return null; + } + } + + /** + * Get the value of the field matching the given name and belonging to target {@link Object}. + * + * @param target target {@link Object} whose field we are retrieving the value from + * @param fieldName field name + * @param field type + * @return field value + */ + public static T getFieldValue(Object target, String fieldName) { + try { + Field field = getField(target.getClass(), fieldName); + @SuppressWarnings("unchecked") + T returnValue = (T) field.get(target); + return returnValue; + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Get the value of the field matching the given name and belonging to target {@link Object} or {@code null} + * if no {@link Field} was found.. + * + * @param target target {@link Object} whose field we are retrieving the value from + * @param fieldName field name + * @param field type + * @return field value matching the given name or {@code null} + */ + public static T getFieldValueOrNull(Object target, String fieldName) { + try { + Field field = getField(target.getClass(), fieldName); + @SuppressWarnings("unchecked") + T returnValue = (T) field.get(target); + return returnValue; + } catch (IllegalAccessException e) { + return null; + } + } + + /** + * Set the value of the field matching the given name and belonging to target {@link Object}. + * + * @param target target object + * @param fieldName field name + * @param value field value + */ + public static void setFieldValue(Object target, String fieldName, Object value) { + try { + Field field = getField(target.getClass(), fieldName); + field.set(target, value); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Object}. + * + * @param target target {@link Object} + * @param methodName method name + * @param parameterTypes method parameter types + * @return return {@link Method} matching the provided signature + */ + public static Method getMethod(Object target, String methodName, Class... parameterTypes) { + return getMethod(target.getClass(), methodName, parameterTypes); + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Object} or {@code null} if no {@link Method} was found. + * + * @param target target {@link Object} + * @param methodName method name + * @param parameterTypes method parameter types + * @return return {@link Method} matching the provided signature or {@code null} + */ + public static Method getMethodOrNull(Object target, String methodName, Class... parameterTypes) { + try { + return getMethod(target.getClass(), methodName, parameterTypes); + } catch (RuntimeException e) { + return null; + } + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Class}. + * + * @param targetClass target {@link Class} + * @param methodName method name + * @param parameterTypes method parameter types + * @return the {@link Method} matching the provided signature + */ + @SuppressWarnings("unchecked") + public static Method getMethod(Class targetClass, String methodName, Class... parameterTypes) { + try { + return targetClass.getDeclaredMethod(methodName, parameterTypes); + } catch (NoSuchMethodException e) { + try { + return targetClass.getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException ignore) { + } + + if (!targetClass.getSuperclass().equals(Object.class)) { + return getMethod(targetClass.getSuperclass(), methodName, parameterTypes); + } else { + throw handleException(e); + } + } + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Object} or {@code null} if no {@link Method} was found. + * + * @param targetClass target {@link Class} + * @param methodName method name + * @param parameterTypes method parameter types + * @return return {@link Method} matching the provided signature or {@code null} + */ + public static Method getMethodOrNull(Class targetClass, String methodName, Class... parameterTypes) { + try { + return getMethod(targetClass, methodName, parameterTypes); + } catch (RuntimeException e) { + return null; + } + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Class}, excluding inherited ones, or {@code null} if no {@link Method} was found. + * + * @param targetClass target {@link Class} + * @param methodName method name + * @param parameterTypes method parameter types + * @return return {@link Method} matching the provided signature or {@code null} + */ + public static Method getDeclaredMethodOrNull(Class targetClass, String methodName, Class... parameterTypes) { + try { + return targetClass.getDeclaredMethod(methodName, parameterTypes); + } catch (NoSuchMethodException e) { + return null; + } + } + + /** + * Check if the provided Java {@link Class} contains a method matching + * the given signature (name and parameter types). + * + * @param targetClass target {@link Class} + * @param methodName method name + * @param parameterTypes method parameter types + * @return the provided Java {@link Class} contains a method with the given signature + */ + public static boolean hasMethod(Class targetClass, String methodName, Class... parameterTypes) { + try { + targetClass.getMethod(methodName, parameterTypes); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + /** + * Get the property setter {@link Method} with the given signature (name and parameter types) + * belonging to the provided Java {@link Object}. + * + * @param target target {@link Object} + * @param propertyName property name + * @param parameterType setter property type + * @return the setter {@link Method} matching the provided signature + */ + public static Method getSetter(Object target, String propertyName, Class parameterType) { + String setterMethodName = SETTER_PREFIX + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + Method setter = getMethod(target, setterMethodName, parameterType); + setter.setAccessible(true); + return setter; + } + + /** + * Get the property getter {@link Method} with the given name belonging to + * the provided Java {@link Object}. + * + * @param target target {@link Object} + * @param propertyName property name + * @return the getter {@link Method} matching the provided name + */ + public static Method getGetter(Object target, String propertyName) { + String getterMethodName = GETTER_PREFIX + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + Method getter = getMethod(target, getterMethodName); + getter.setAccessible(true); + return getter; + } + + /** + * Invoke the provided {@link Method} on the given Java {@link Object}. + * + * @param target target {@link Object} whose method we are invoking + * @param method method to invoke + * @param parameters parameters passed to the method call + * @param return value object type + * @return the value return by the {@link Method} invocation + */ + public static T invokeMethod(Object target, Method method, Object... parameters) { + try { + method.setAccessible(true); + @SuppressWarnings("unchecked") + T returnValue = (T) method.invoke(target, parameters); + return returnValue; + } catch (InvocationTargetException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Invoke the method with the provided signature (name and parameter types) + * on the given Java {@link Object}. + * + * @param target target {@link Object} whose method we are invoking + * @param methodName method name to invoke + * @param parameters parameters passed to the method call + * @param return value object type + * @return the value return by the method invocation + */ + public static T invokeMethod(Object target, String methodName, Object... parameters) { + try { + Class[] parameterClasses = new Class[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + parameterClasses[i] = parameters[i].getClass(); + } + + Method method = getMethod(target, methodName, parameterClasses); + method.setAccessible(true); + @SuppressWarnings("unchecked") + T returnValue = (T) method.invoke(target, parameters); + return returnValue; + } catch (InvocationTargetException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Invoke the property getter with the provided name on the given Java {@link Object}. + * + * @param target target {@link Object} whose property getter we are invoking + * @param propertyName property name whose getter we are invoking + * @param return value object type + * @return the value return by the getter invocation + */ + public static T invokeGetter(Object target, String propertyName) { + Method setter = getGetter(target, propertyName); + try { + return (T) setter.invoke(target); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Invoke the property setter with the provided signature (name and parameter types) + * on the given Java {@link Object}. + * + * @param target target {@link Object} whose property setter we are invoking + * @param propertyName property name whose setter we are invoking + * @param parameter parameter passed to the setter call + */ + public static void invokeSetter(Object target, String propertyName, Object parameter) { + Method setter = getSetter(target, propertyName, parameter.getClass()); + try { + setter.invoke(target, parameter); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Invoke the {@link boolean} property setter with the provided name + * on the given Java {@link Object}. + * + * @param target target {@link Object} whose property setter we are invoking + * @param propertyName property name whose setter we are invoking + * @param parameter {@link boolean} parameter passed to the setter call + */ + public static void invokeSetter(Object target, String propertyName, boolean parameter) { + Method setter = getSetter(target, propertyName, boolean.class); + try { + setter.invoke(target, parameter); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Invoke the {@link int} property setter with the provided name + * on the given Java {@link Object}. + * + * @param target target {@link Object} whose property setter we are invoking + * @param propertyName property name whose setter we are invoking + * @param parameter {@link int} parameter passed to the setter call + */ + public static void invokeSetter(Object target, String propertyName, int parameter) { + Method setter = getSetter(target, propertyName, int.class); + try { + setter.invoke(target, parameter); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Invoke the {@code static} {@link Method} with the provided parameters. + * + * @param method target {@code static} {@link Method} to invoke + * @param parameters parameters passed to the method call + * @param return value object type + * @return the value return by the method invocation + */ + public static T invokeStaticMethod(Method method, Object... parameters) { + try { + method.setAccessible(true); + @SuppressWarnings("unchecked") + T returnValue = (T) method.invoke(null, parameters); + return returnValue; + } catch (InvocationTargetException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Get the Java {@link Class} with the given fully-qualified name. + * + * @param className the Java {@link Class} name to be retrieved + * @param {@link Class} type + * @return the Java {@link Class} object + */ + @SuppressWarnings("unchecked") + public static Class getClass(String className) { + try { + return (Class) Class.forName(className, false, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw handleException(e); + } + } + + /** + * Get the Java {@link Class} with the given fully-qualified name or or {@code null} + * if no {@link Class} was found matching the provided name. + * + * @param className the Java {@link Class} name to be retrieved + * @param {@link Class} type + * @return the Java {@link Class} object or {@code null} + */ + @SuppressWarnings("unchecked") + public static Class getClassOrNull(String className) { + try { + return (Class) getClass(className); + } catch (Exception e) { + return null; + } + } + + /** + * Get the Java Wrapper {@link Class} associated to the given primitive type. + * + * @param clazz primitive class + * @return the Java Wrapper {@link Class} + */ + public static Class getWrapperClass(Class clazz) { + if (!clazz.isPrimitive()) + return clazz; + + if (clazz == Integer.TYPE) + return Integer.class; + if (clazz == Long.TYPE) + return Long.class; + if (clazz == Boolean.TYPE) + return Boolean.class; + if (clazz == Byte.TYPE) + return Byte.class; + if (clazz == Character.TYPE) + return Character.class; + if (clazz == Float.TYPE) + return Float.class; + if (clazz == Double.TYPE) + return Double.class; + if (clazz == Short.TYPE) + return Short.class; + if (clazz == Void.TYPE) + return Void.class; + + return clazz; + } + + /** + * Get the first super class matching the provided package name. + * + * @param clazz Java class + * @param packageName package name + * @param class generic type + * @return the first super class matching the provided package name or {@code null}. + */ + public static Class getFirstSuperClassFromPackage(Class clazz, String packageName) { + if (clazz.getPackage().getName().equals(packageName)) { + return clazz; + } else { + Class superClass = clazz.getSuperclass(); + return (superClass == null || superClass.equals(Object.class)) ? + null : + (Class) getFirstSuperClassFromPackage(superClass, packageName); + } + } + + /** + * Get the generic types of a given Class. + * + * @param parameterizedType parameterized Type + * @return generic types for the given Class. + */ + public static Set getGenericTypes(ParameterizedType parameterizedType) { + Set genericTypes = new LinkedHashSet<>(); + for(Type genericType : parameterizedType.getActualTypeArguments()) { + if (genericType instanceof Class) { + genericTypes.add((Class) genericType); + } + } + return genericTypes; + } + + /** + * Get the {@link Member} with the given name belonging to the provided Java {@link Class} or {@code null} + * if no {@link Member} was found. + * + * @param targetClass the provided Java {@link Class} the field or method belongs to + * @param memberName the {@link Field} or {@link Method} name + * @return the {@link Field} or {@link Method} matching the given name or {@code null} + */ + public static Member getMemberOrNull(Class targetClass, String memberName) { + Field field = getFieldOrNull(targetClass, memberName); + return (field != null) ? field : getMethodOrNull(targetClass, memberName); + } + + /** + * Get the generic {@link Type} of the {@link Member} with the given name belonging to the provided Java {@link Class} or {@code null} + * if no {@link Member} was found. + * + * @param targetClass the provided Java {@link Class} the field or method belongs to + * @param memberName the {@link Field} or {@link Method} name + * @return the generic {@link Type} of the {@link Field} or {@link Method} matching the given name or {@code null} + */ + public static Type getMemberGenericTypeOrNull(Class targetClass, String memberName) { + Field field = getFieldOrNull(targetClass, memberName); + return (field != null) ? field.getGenericType() : getMethodOrNull(targetClass, memberName).getGenericReturnType(); + } + + /** + * Handle the {@link NoSuchFieldException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link NoSuchFieldException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(NoSuchFieldException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link NoSuchMethodException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link NoSuchMethodException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(NoSuchMethodException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link IllegalAccessException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link IllegalAccessException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(IllegalAccessException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link InvocationTargetException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link InvocationTargetException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(InvocationTargetException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link ClassNotFoundException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link ClassNotFoundException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(ClassNotFoundException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link InstantiationException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link InstantiationException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(InstantiationException e) { + return new IllegalArgumentException(e); + } +} diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java new file mode 100644 index 000000000..473aff942 --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/util/StringUtils.java @@ -0,0 +1,41 @@ +package com.vladmihalcea.hibernate.util; + +/** + * StringUtils - String utilities holder. + * + * @author Vlad Mihalcea + * @since 2.5.1 + */ +public class StringUtils { + + public static final String LINE_SEPARATOR = System.getProperty("line.separator"); + + private StringUtils() { + throw new UnsupportedOperationException("StringUtils is not instantiable!"); + } + + /** + * Join the provided {@code elements} separated by the {@code delimiter}. + * + * @param delimiter delimiter + * @param elements elements to join + * @return the {link @String} result obtained from joining all elements + */ + public static String join(CharSequence delimiter, CharSequence... elements) { + StringBuilder builder = new StringBuilder(); + + boolean first = true; + + for (CharSequence element : elements) { + if(first) { + first = false; + } else { + builder.append(delimiter); + } + builder.append(element); + } + + return builder.toString(); + } + +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/AbstractBatchSequenceGeneratorTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/AbstractBatchSequenceGeneratorTest.java new file mode 100644 index 000000000..85b6b3dda --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/AbstractBatchSequenceGeneratorTest.java @@ -0,0 +1,128 @@ +package com.vladmihalcea.hibernate.id; + +import com.vladmihalcea.hibernate.util.AbstractTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Parameter; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Philippe Marschall + */ +public abstract class AbstractBatchSequenceGeneratorTest extends AbstractTest { + + private static final int BATCH_SIZE = 50; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected abstract DataSourceProvider dataSourceProvider(); + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.BATCH_VERSIONED_DATA, true); + properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, BATCH_SIZE); + properties.put(AvailableSettings.ORDER_UPDATES, true); + properties.put(AvailableSettings.ORDER_INSERTS, true); + } + + @Test + public void test() { + + QueryCountHolder.clear(); + doInJPA(entityManager -> { + for (int i = 0; i < BATCH_SIZE; i++) { + Post post = new Post(); + + post.setTitle("Post " + i + 1); + entityManager.persist(post); + } + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1L, queryCount.getInsert()); + assertEquals(1L, getSelectCount(queryCount)); + assertEquals(2L, queryCount.getTotal()); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery("SELECT p FROM Post p", Post.class).getResultList(); + assertEquals(BATCH_SIZE, posts.size()); + }); + } + + /** + * Recursive CTEs are not always recognized as OTHER instead of SELECT by datasource-proxy. + * See @link https://github.com/ttddyy/datasource-proxy/issues/76 + * + * @param queryCount query count + * @return select statement count + */ + protected long getSelectCount(QueryCount queryCount) { + switch (dataSourceProvider().database()) { + case ORACLE: + return queryCount.getSelect(); + case SQLSERVER: + case H2: + case POSTGRESQL: + return queryCount.getOther(); + } + throw new UnsupportedOperationException( + "Unsupported database: " + dataSourceProvider().database() + ); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GenericGenerator( + name = "post_sequence", + strategy = "com.vladmihalcea.hibernate.id.BatchSequenceGenerator", + parameters = { + @Parameter(name = "sequence", value = "SEQ_PARENT_ID"), + @Parameter(name = "fetch_size", value = "" + BATCH_SIZE) + } + ) + @GeneratedValue(generator = "post_sequence") + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/H2BatchSequenceGeneratorTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/H2BatchSequenceGeneratorTest.java new file mode 100644 index 000000000..4a1a5a037 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/H2BatchSequenceGeneratorTest.java @@ -0,0 +1,15 @@ +package com.vladmihalcea.hibernate.id; + +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.H2DataSourceProvider; + +/** + * @author Philippe Marschall + */ +public class H2BatchSequenceGeneratorTest extends AbstractBatchSequenceGeneratorTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new H2DataSourceProvider(); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/OracleBatchSequenceGeneratorTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/OracleBatchSequenceGeneratorTest.java new file mode 100644 index 000000000..1e141e282 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/OracleBatchSequenceGeneratorTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hibernate.id; + +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.OracleDataSourceProvider; + +/** + * @author Philippe Marschall + */ +public class OracleBatchSequenceGeneratorTest extends AbstractBatchSequenceGeneratorTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new OracleDataSourceProvider(); + } + +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/PostgreSQLBatchSequenceGeneratorTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/PostgreSQLBatchSequenceGeneratorTest.java new file mode 100644 index 000000000..4cf34ef58 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/PostgreSQLBatchSequenceGeneratorTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hibernate.id; + +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; + +/** + * @author Philippe Marschall + */ +public class PostgreSQLBatchSequenceGeneratorTest extends AbstractBatchSequenceGeneratorTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider(); + } + +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/SQLServerBatchSequenceGeneratorTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/SQLServerBatchSequenceGeneratorTest.java new file mode 100644 index 000000000..a8bd788bd --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/id/SQLServerBatchSequenceGeneratorTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hibernate.id; + +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.SQLServerDataSourceProvider; + +/** + * @author Philippe Marschall + */ +public class SQLServerBatchSequenceGeneratorTest extends AbstractBatchSequenceGeneratorTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new SQLServerDataSourceProvider(); + } + +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/naming/CamelCaseToSnakeCaseNamingStrategyTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/naming/CamelCaseToSnakeCaseNamingStrategyTest.java new file mode 100644 index 000000000..0d21d8717 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/naming/CamelCaseToSnakeCaseNamingStrategyTest.java @@ -0,0 +1,160 @@ +package com.vladmihalcea.hibernate.naming; + +import com.vladmihalcea.hibernate.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import java.time.LocalDate; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class CamelCaseToSnakeCaseNamingStrategyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + BookAuthor.class, + PaperBackBook.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.physical_naming_strategy", + com.vladmihalcea.hibernate.naming.CamelCaseToSnakeCaseNamingStrategy.INSTANCE + ); + } + + @Test + public void test() { + doInJPA(entityManager -> { + BookAuthor author = new BookAuthor(); + author.setId(1L); + author.setFirstName("Vlad"); + author.setLastName("Mihalcea"); + + entityManager.persist(author); + + PaperBackBook book = new PaperBackBook(); + book.setISBN("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(LocalDate.of(2016, 10, 12)); + book.setPublishedBy(author); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + PaperBackBook book = session.bySimpleNaturalId(PaperBackBook.class).load("978-9730228236"); + assertEquals("High-Performance Java Persistence", book.getTitle()); + + assertEquals("Vlad Mihalcea", book.getPublishedBy().getFullName()); + }); + } + + @Entity(name = "BookAuthor") + public static class BookAuthor { + + @Id + private Long id; + + private String firstName; + + private String lastName; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getFullName() { + return firstName + " " + lastName; + } + } + + @Entity(name = "PaperBackBook") + public static class PaperBackBook { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @NaturalId + private String ISBN; + + private String title; + + private LocalDate publishedOn; + + @ManyToOne(fetch = FetchType.LAZY) + private BookAuthor publishedBy; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getISBN() { + return ISBN; + } + + public void setISBN(String ISBN) { + this.ISBN = ISBN; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(LocalDate publishedOn) { + this.publishedOn = publishedOn; + } + + public BookAuthor getPublishedBy() { + return publishedBy; + } + + public void setPublishedBy(BookAuthor publishedBy) { + this.publishedBy = publishedBy; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/ListResultTransformerTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/ListResultTransformerTest.java new file mode 100644 index 000000000..b1d2a7ed0 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/ListResultTransformerTest.java @@ -0,0 +1,212 @@ +package com.vladmihalcea.hibernate.query; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.transform.ResultTransformer; +import org.junit.Test; + +import java.time.LocalDate; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ListResultTransformerTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle( + "High-Performance Java Persistence " + + "eBook has been released!") + .setCreatedOn(LocalDate.of(2016, 8, 30)) + ); + + entityManager.persist( + new Post() + .setId(2L) + .setTitle( + "High-Performance Java Persistence " + + "paperback has been released!") + .setCreatedOn(LocalDate.of(2016, 10, 12)) + ); + + entityManager.persist( + new Post() + .setId(3L) + .setTitle( + "High-Performance Java Persistence " + + "Mach 1 video course has been released!") + .setCreatedOn(LocalDate.of(2018, 1, 30)) + ); + + entityManager.persist( + new Post() + .setId(4L) + .setTitle( + "High-Performance Java Persistence " + + "Mach 2 video course has been released!") + .setCreatedOn(LocalDate.of(2018, 5, 8)) + ); + + entityManager.persist( + new Post() + .setId(5L) + .setTitle( + "Hypersistence Optimizer has been released!") + .setCreatedOn(LocalDate.of(2019, 3, 19)) + ); + }); + } + + public static class PostCountByYear { + + private final int year; + + private final int postCount; + + public PostCountByYear(int year, int postCount) { + this.year = year; + this.postCount = postCount; + } + + public int getYear() { + return year; + } + + public int getPostCount() { + return postCount; + } + } + + @Test + public void testTransformer() { + doInJPA(entityManager -> { + List postCountByYearMap = (List) entityManager + .createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from Post p " + + "group by " + + " YEAR(p.createdOn) " + + "order by " + + " YEAR(p.createdOn)") + .unwrap(org.hibernate.query.Query.class) + .setResultTransformer( + new ResultTransformer() { + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + return new PostCountByYear( + ((Number) tuple[0]).intValue(), + ((Number) tuple[1]).intValue() + ); + } + + @Override + public List transformList(List tuples) { + return tuples; + } + } + ) + .getResultList(); + + assertEquals(2016, postCountByYearMap.get(0).getYear()); + assertEquals(2, postCountByYearMap.get(0).getPostCount()); + + assertEquals(2018, postCountByYearMap.get(1).getYear()); + assertEquals(2, postCountByYearMap.get(1).getPostCount()); + + assertEquals(2019, postCountByYearMap.get(2).getYear()); + assertEquals(1, postCountByYearMap.get(2).getPostCount()); + }); + } + + @Test + public void testListResultTransformer() { + doInJPA(entityManager -> { + List postCountByYearMap = (List) entityManager + .createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from Post p " + + "group by " + + " YEAR(p.createdOn) " + + "order by " + + " YEAR(p.createdOn)") + .unwrap(org.hibernate.query.Query.class) + .setResultTransformer( + (com.vladmihalcea.hibernate.type.util.ListResultTransformer) (tuple, aliases) -> new PostCountByYear( + ((Number) tuple[0]).intValue(), + ((Number) tuple[1]).intValue() + ) + ) + .getResultList(); + + assertEquals(2016, postCountByYearMap.get(0).getYear()); + assertEquals(2, postCountByYearMap.get(0).getPostCount()); + + assertEquals(2018, postCountByYearMap.get(1).getYear()); + assertEquals(2, postCountByYearMap.get(1).getPostCount()); + + assertEquals(2019, postCountByYearMap.get(2).getYear()); + assertEquals(1, postCountByYearMap.get(2).getPostCount()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDate createdOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDate getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDate createdOn) { + this.createdOn = createdOn; + return this; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/MapResultTransformerTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/MapResultTransformerTest.java new file mode 100644 index 000000000..87638418e --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/MapResultTransformerTest.java @@ -0,0 +1,219 @@ +package com.vladmihalcea.hibernate.query; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.time.LocalDate; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MapResultTransformerTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle( + "High-Performance Java Persistence eBook " + + "has been released!" + ) + .setCreatedOn(LocalDate.of(2016, 8, 30)) + ); + + entityManager.persist( + new Post() + .setId(2L) + .setTitle( + "High-Performance Java Persistence paperback " + + "has been released!" + ) + .setCreatedOn(LocalDate.of(2016, 10, 12)) + ); + + entityManager.persist( + new Post() + .setId(3L) + .setTitle( + "High-Performance Java Persistence Mach 1 video course " + + "has been released!" + ) + .setCreatedOn(LocalDate.of(2018, 1, 30)) + ); + + entityManager.persist( + new Post() + .setId(4L) + .setTitle( + "High-Performance Java Persistence Mach 2 video course " + + "has been released!" + ) + .setCreatedOn(LocalDate.of(2018, 5, 8)) + ); + + entityManager.persist( + new Post() + .setId(5L) + .setTitle( + "Hypersistence Optimizer " + + "has been released!" + ) + .setCreatedOn(LocalDate.of(2019, 3, 19)) + ); + }); + } + + @Test + public void testGroupByStreamCollector() { + doInJPA(entityManager -> { + Map postCountByYearMap = entityManager + .createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from " + + " Post p " + + "group by " + + " YEAR(p.createdOn)", Tuple.class) + .getResultStream() + .collect( + Collectors.toMap( + tuple -> ((Number) tuple.get("year")).intValue(), + tuple -> ((Number) tuple.get("postCount")).intValue() + ) + ); + + assertEquals(2, postCountByYearMap.get(2016).intValue()); + assertEquals(2, postCountByYearMap.get(2018).intValue()); + assertEquals(1, postCountByYearMap.get(2019).intValue()); + }); + } + + @Test + public void testGroupByListStreamCollector() { + doInJPA(entityManager -> { + Map postCountByYearMap = entityManager + .createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from " + + " Post p " + + "group by " + + " YEAR(p.createdOn)", Tuple.class) + .getResultList() + .stream() + .collect( + Collectors.toMap( + tuple -> ((Number) tuple.get("year")).intValue(), + tuple -> ((Number) tuple.get("postCount")).intValue() + ) + ); + + assertEquals(2, postCountByYearMap.get(2016).intValue()); + assertEquals(2, postCountByYearMap.get(2018).intValue()); + assertEquals(1, postCountByYearMap.get(2019).intValue()); + }); + } + + @Test + public void testMapResultTransformerImplicitAlias() { + doInJPA(entityManager -> { + Map postCountByYearMap = (Map) entityManager + .createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from " + + " Post p " + + "group by " + + " YEAR(p.createdOn)") + .unwrap(org.hibernate.query.Query.class) + .setResultTransformer( + new com.vladmihalcea.hibernate.type.util.MapResultTransformer() + ) + .getSingleResult(); + + assertEquals(2, postCountByYearMap.get(2016).intValue()); + assertEquals(2, postCountByYearMap.get(2018).intValue()); + assertEquals(1, postCountByYearMap.get(2019).intValue()); + }); + } + + @Test + public void testMapResultTransformerExplicitAlias() { + doInJPA(entityManager -> { + Map postCountByYearMap = (Map) entityManager + .createQuery( + "select " + + " count(p) as map_value, " + + " YEAR(p.createdOn) as map_key " + + "from Post p " + + "group by " + + " YEAR(p.createdOn)") + .unwrap(org.hibernate.query.Query.class) + .setResultTransformer( + new com.vladmihalcea.hibernate.type.util.MapResultTransformer() + ) + .getSingleResult(); + + assertEquals(2, postCountByYearMap.get(2016).intValue()); + assertEquals(2, postCountByYearMap.get(2018).intValue()); + assertEquals(1, postCountByYearMap.get(2019).intValue()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDate createdOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDate getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDate createdOn) { + this.createdOn = createdOn; + return this; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/SQLExtractorTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/SQLExtractorTest.java new file mode 100644 index 000000000..8008baa7b --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/SQLExtractorTest.java @@ -0,0 +1,164 @@ +package com.vladmihalcea.hibernate.query; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; +import org.junit.Test; + +import java.time.LocalDate; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class SQLExtractorTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Test + public void testJPQL() { + doInJPA(entityManager -> { + Query jpql = entityManager + .createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from " + + " Post p " + + "group by " + + " YEAR(p.createdOn)", Tuple.class); + + String sql = SQLExtractor.from(jpql); + + assertNotNull(sql); + + LOGGER.info( + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testCriteriaAPI() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery criteria = builder.createQuery(PostComment.class); + + Root postComment = criteria.from(PostComment.class); + Join post = postComment.join("post"); + + criteria.where( + builder.like(post.get("title"), "%Java%") + ); + + criteria.orderBy( + builder.asc(postComment.get("id")) + ); + + Query criteriaQuery = entityManager.createQuery(criteria); + + String sql = SQLExtractor.from(criteriaQuery); + + assertNotNull(sql); + + LOGGER.info( + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDate createdOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDate getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDate createdOn) { + this.createdOn = createdOn; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/ArrayTypeNativeQueryTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/ArrayTypeNativeQueryTest.java new file mode 100644 index 000000000..2959c89bf --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/ArrayTypeNativeQueryTest.java @@ -0,0 +1,156 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.*; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ArrayTypeNativeQueryTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return PostgreSQL95ArrayDialect.class.getName(); + } + }; + } + + //@Ignore("TODO: Unsupported YET!!!") + /*@Override + protected List additionalTypes() { + return Arrays.asList( + StringArrayType.INSTANCE, + IntArrayType.INSTANCE + ); + }*/ + + @Test + public void test() { + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + event.setSensorNames(new String[]{"Temperature", "Pressure"}); + event.setSensorValues(new int[]{12, 756}); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + List events = entityManager + .createNamedQuery("EventIdSensorValues", EventSensors.class) + .getResultList(); + + assertEquals(2, events.size()); + }); + } + + @Entity(name = "Event") + /*@TypeDef(name = "sensor-state-array", typeClass = EnumArrayType.class, parameters = { + @Parameter(name = EnumArrayType.SQL_ARRAY_TYPE, value = "sensor_state")} + )*/ + @Table(name = "event") + @NamedNativeQuery( + name = "EventIdSensorValues", + query = "select " + + " id, " + + " sensor_names, " + + " sensor_values " + + "from event ", + resultSetMapping = "EventIdSensorValues" + ) + @SqlResultSetMapping( + name = "EventIdSensorValues", + classes = @ConstructorResult( + targetClass = EventSensors.class, + columns = { + @ColumnResult( + name = "id", + type = Long.class + ), + @ColumnResult( + name = "sensor_names", + type = String[].class + ), + @ColumnResult( + name = "sensor_values", + type = int[].class + ) + } + ) + ) + public static class Event extends BaseEntity { + + @Type(StringArrayType.class) + @Column(name = "sensor_names", columnDefinition = "text[]") + private String[] sensorNames; + + @Type(IntArrayType.class) + @Column(name = "sensor_values", columnDefinition = "integer[]") + private int[] sensorValues; + + public String[] getSensorNames() { + return sensorNames; + } + + public void setSensorNames(String[] sensorNames) { + this.sensorNames = sensorNames; + } + + public int[] getSensorValues() { + return sensorValues; + } + + public void setSensorValues(int[] sensorValues) { + this.sensorValues = sensorValues; + } + } + + public static class EventSensors { + + private Long id; + + private String[] sensorNames; + + private int[] sensorValues; + + public EventSensors(Long id, String[] sensorNames, int[] sensorValues) { + this.id = id; + this.sensorNames = sensorNames; + this.sensorValues = sensorValues; + } + + public Long getId() { + return id; + } + + public String[] getSensorNames() { + return sensorNames; + } + + public int[] getSensorValues() { + return sensorValues; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/ArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/ArrayTypeTest.java new file mode 100644 index 000000000..528179b41 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/ArrayTypeTest.java @@ -0,0 +1,315 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import javax.sql.DataSource; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.Date; +import java.util.UUID; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + * @author Guillaume Briand + */ +public class ArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + public void init() { + DataSource dataSource = newDataSource(); + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + try { + statement.executeUpdate( + "DROP TYPE sensor_state CASCADE" + ); + } catch (SQLException ignore) { + } + statement.executeUpdate( + "CREATE TYPE sensor_state AS ENUM ('ONLINE', 'OFFLINE', 'UNKNOWN')" + ); + statement.executeUpdate( + "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"" + ); + } catch (SQLException e) { + fail(e.getMessage()); + } + super.init(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return PostgreSQL95ArrayDialect.class.getName(); + } + }; + } + + @Test + public void test() { + + Date date1 = Date.from(LocalDate.of(1991, 12, 31).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + Date date2 = Date.from(LocalDate.of(1990, 1, 1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + event.setSensorIds(new UUID[]{UUID.fromString("c65a3bcb-8b36-46d4-bddb-ae96ad016eb1"), UUID.fromString("72e95717-5294-4c15-aa64-a3631cf9a800")}); + event.setSensorNames(new String[]{"Temperature", "Pressure"}); + event.setSensorValues(new int[]{12, 756}); + event.setSensorLongValues(new long[]{42L, 9223372036854775800L}); + event.setSensorDoubleValues(new double[]{0.123, 456.789}); + event.setSensorStates(new SensorState[]{SensorState.ONLINE, SensorState.OFFLINE, SensorState.ONLINE, SensorState.UNKNOWN}); + event.setDateValues(new Date[]{date1, date2}); + event.setTimestampValues(new Date[]{date1, date2}); + event.setDecimalValues(new BigDecimal[]{BigDecimal.ONE, BigDecimal.ZERO, BigDecimal.TEN}); + event.setLocalDateValues(new LocalDate[]{LocalDate.of(2022, 3, 22), LocalDate.of(2021, 4, 21)}); + event.setLocalDateTimeValues(new LocalDateTime[]{LocalDateTime.of(2022, 3, 22, 11, 22, 33), LocalDateTime.of(2021, 4, 21, 22, 33, 44)}); + event.setSensorBooleanValues(new Boolean[]{false, true, true}); + + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals(new UUID[]{UUID.fromString("c65a3bcb-8b36-46d4-bddb-ae96ad016eb1"), UUID.fromString("72e95717-5294-4c15-aa64-a3631cf9a800")}, event.getSensorIds()); + assertArrayEquals(new String[]{"Temperature", "Pressure"}, event.getSensorNames()); + assertArrayEquals(new int[]{12, 756}, event.getSensorValues()); + assertArrayEquals(new long[]{42L, 9223372036854775800L}, event.getSensorLongValues()); + assertArrayEquals(new double[]{0.123, 456.789}, event.getSensorDoubleValues(), 0.01); + assertArrayEquals(new SensorState[]{SensorState.ONLINE, SensorState.OFFLINE, SensorState.ONLINE, SensorState.UNKNOWN}, event.getSensorStates()); + assertArrayEquals(new Date[]{date1, date2}, event.getDateValues()); + assertArrayEquals(new Date[]{date1, date2}, event.getTimestampValues()); + assertArrayEquals(new BigDecimal[]{BigDecimal.ONE, BigDecimal.ZERO, BigDecimal.TEN}, event.getDecimalValues()); + assertArrayEquals(new LocalDate[]{LocalDate.of(2022, 3, 22), LocalDate.of(2021, 4, 21)}, event.getLocalDateValues()); + assertArrayEquals(new LocalDateTime[]{LocalDateTime.of(2022, 3, 22, 11, 22, 33), LocalDateTime.of(2021, 4, 21, 22, 33, 44)}, event.getLocalDateTimeValues()); + assertArrayEquals(new Boolean[]{false, true, true}, event.getSensorBooleanValues()); + }); + + //@Ignore("TODO: Unsupported YET!!!") + /*doInJPA(entityManager -> { + List events = entityManager.createNativeQuery( + "select " + + " id, " + + " sensor_ids, " + + " sensor_names, " + + " sensor_values, " + + " date_values " + + "from event ", Tuple.class) + .unwrap(org.hibernate.query.NativeQuery.class) + .addScalar("sensor_ids", UUIDArrayType.INSTANCE) + .addScalar("sensor_names", StringArrayType.INSTANCE) + .addScalar("sensor_values", IntArrayType.INSTANCE) + .addScalar("date_values", DateArrayType.INSTANCE) + .getResultList(); + + assertEquals(2, events.size()); + });*/ + } + + @Test + public void testLargeArray() { + int[] sensorValues = new int[100]; + + Arrays.fill(sensorValues, 0, 10, 123); + Arrays.fill(sensorValues, 10, 50, 456); + Arrays.fill(sensorValues, 50, 90, 789); + Arrays.fill(sensorValues, 90, 100, 0); + + doInJPA(entityManager -> { + Event event = new Event(); + event.setId(0L); + event.setSensorValues(sensorValues); + + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 0L); + + assertArrayEquals(sensorValues, event.getSensorValues()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + /*@TypeDef(name = "sensor-state-array", typeClass = EnumArrayType.class, parameters = { + @Parameter(name = EnumArrayType.SQL_ARRAY_TYPE, value = "sensor_state")} + )*/ + public static class Event extends BaseEntity { + @Type(UUIDArrayType.class) + @Column(name = "sensor_ids", columnDefinition = "uuid[]") + private UUID[] sensorIds; + + @Type(StringArrayType.class) + @Column(name = "sensor_names", columnDefinition = "text[]") + private String[] sensorNames; + + @Type(IntArrayType.class) + @Column(name = "sensor_values", columnDefinition = "integer[]") + private int[] sensorValues; + + @Type(LongArrayType.class) + @Column(name = "sensor_long_values", columnDefinition = "bigint[]") + private long[] sensorLongValues; + + @Type(BooleanArrayType.class) + @Column(name = "sensor_boolean_values", columnDefinition = "boolean[]") + private Boolean[] sensorBooleanValues; + + @Type(DoubleArrayType.class) + @Column(name = "sensor_double_values", columnDefinition = "float8[]") + private double[] sensorDoubleValues; + + @Type(DoubleArrayType.class) + @Column(name = "date_values", columnDefinition = "date[]") + private Date[] dateValues; + + @Type(TimestampArrayType.class) + @Column(name = "timestamp_values", columnDefinition = "timestamp[]") + private Date[] timestampValues; + + @Type(DecimalArrayType.class) + @Column(name = "decimal_values", columnDefinition = "decimal[]") + private BigDecimal[] decimalValues; + + @Type(LocalDateArrayType.class) + @Column(name = "localdate_values", columnDefinition = "date[]") + private LocalDate[] localDateValues; + + @Type(LocalDateArrayType.class) + @Column(name = "localdatetime_values", columnDefinition = "timestamp[]") + private LocalDateTime[] localDateTimeValues; + + @Type(EnumArrayType.class) + @Column(name = "sensor_states", columnDefinition = "sensor_state[]") + private SensorState[] sensorStates; + + public UUID[] getSensorIds() { + return sensorIds; + } + + public void setSensorIds(UUID[] sensorIds) { + this.sensorIds = sensorIds; + } + + public String[] getSensorNames() { + return sensorNames; + } + + public void setSensorNames(String[] sensorNames) { + this.sensorNames = sensorNames; + } + + public int[] getSensorValues() { + return sensorValues; + } + + public void setSensorValues(int[] sensorValues) { + this.sensorValues = sensorValues; + } + + public long[] getSensorLongValues() { + return sensorLongValues; + } + + public void setSensorLongValues(long[] sensorLongValues) { + this.sensorLongValues = sensorLongValues; + } + + public Boolean[] getSensorBooleanValues() { return sensorBooleanValues; } + + public void setSensorBooleanValues(Boolean[] sensorBooleanValues) { this.sensorBooleanValues = sensorBooleanValues; } + + public double[] getSensorDoubleValues() { + return sensorDoubleValues; + } + + public void setSensorDoubleValues(double[] sensorDoubleValues) { + this.sensorDoubleValues = sensorDoubleValues; + } + + public SensorState[] getSensorStates() { + return sensorStates; + } + + public void setSensorStates(SensorState[] sensorStates) { + this.sensorStates = sensorStates; + } + + public Date[] getDateValues() { + return dateValues; + } + + public void setDateValues(Date[] dateValues) { + this.dateValues = dateValues; + } + + public Date[] getTimestampValues() { + return timestampValues; + } + + public void setTimestampValues(Date[] timestampValues) { + this.timestampValues = timestampValues; + } + + public BigDecimal[] getDecimalValues() { + return decimalValues; + } + + public void setDecimalValues(BigDecimal[] decimalValues) { + this.decimalValues = decimalValues; + } + + public LocalDate[] getLocalDateValues() { + return localDateValues; + } + + public void setLocalDateValues(LocalDate[] localDateValues) { + this.localDateValues = localDateValues; + } + + public LocalDateTime[] getLocalDateTimeValues() { + return localDateTimeValues; + } + + public void setLocalDateTimeValues(LocalDateTime[] localDateTimeValues) { + this.localDateTimeValues = localDateTimeValues; + } + } + + public enum SensorState { + ONLINE, OFFLINE, UNKNOWN; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/BindArrayTypeQueryParameterTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/BindArrayTypeQueryParameterTest.java new file mode 100644 index 000000000..b52d6d573 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/BindArrayTypeQueryParameterTest.java @@ -0,0 +1,209 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.ExceptionUtil; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.ParameterExpression; +import jakarta.persistence.criteria.Root; +import org.hibernate.annotations.Type; +import org.hibernate.query.BindableType; +import org.hibernate.query.TypedParameterValue; +import org.junit.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.Assert.*; + +/** + * @author Stanislav Gubanov + */ +public class BindArrayTypeQueryParameterTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + public void init() { + DataSource dataSource = newDataSource(); + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + statement.executeUpdate( + "CREATE OR REPLACE FUNCTION " + + " fn_array_contains(" + + " left_array integer[], " + + " right_array integer[]" + + ") RETURNS " + + " boolean AS " + + "$$ " + + "BEGIN " + + " return left_array @> right_array; " + + "END; " + + "$$ LANGUAGE 'plpgsql';" + ); + } catch (SQLException e) { + fail(e.getMessage()); + } + super.init(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return PostgreSQL95ArrayDialect.class.getName(); + } + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Event event = new Event(); + event.setId(1L); + event.setName("Temperature"); + event.setValues(new int[]{1, 2, 3}); + entityManager.persist(event); + }); + } + + @Test + public void testJPQLWithDefaultParameterBiding() { + try { + doInJPA(entityManager -> { + Event event = entityManager + .createQuery( + "select e " + + "from Event e " + + "where " + + " fn_array_contains(e.values, :arrayValues) = true", Event.class) + .setParameter("arrayValues", new int[]{2, 3}) + .getSingleResult(); + }); + } catch (Exception e) { + Exception rootCause = ExceptionUtil.rootCause(e); + assertTrue(rootCause.getMessage().contains("ERROR: function fn_array_contains(integer[], bytea) does not exist")); + } + } + + @Test + public void testJPQLWithExplicitParameterTypeBinding() { + //@Ignore("TODO: Unsupported YET!!!") + /*doInJPA(entityManager -> { + Event event = (Event) entityManager + .createQuery( + "select e " + + "from Event e " + + "where " + + " fn_array_contains(e.values, :arrayValues) = true", Event.class) + .unwrap(org.hibernate.query.Query.class) + .setParameter("arrayValues", new int[]{2, 3}, IntArrayType.INSTANCE) + .getSingleResult(); + + assertArrayEquals(new int[]{1, 2, 3}, event.getValues()); + });*/ + } + + @Test + public void testJPQLWithTypedParameterValue() { + //("TODO: Unsupported YET!!! MAYBE SOLUTION????") + doInJPA(entityManager -> { + Event event = entityManager + .createQuery( + "select e " + + "from Event e " + + "where " + + " fn_array_contains(e.values, :arrayValues) = true", Event.class) + .setParameter("arrayValues", new TypedParameterValue<>(IntArrayType.INSTANCE, new int[]{2, 3})) + .getSingleResult(); + + assertArrayEquals(new int[]{1, 2, 3}, event.getValues()); + }); + } + + @Test + public void testCriteriaAPI() { + doInJPA(entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(Event.class); + Root root = cq.from(Event.class); + cq.select(root); + + ParameterExpression containValues = cb.parameter(int[].class, "arrayValues"); + cq.where( + cb.equal( + cb.function( + "fn_array_contains", + Boolean.class, + root.get("values"), containValues + ), + Boolean.TRUE + ) + ); + + //@Ignore("TODO: Unsupported YET!!!") + /*Event event = (Event) entityManager + .createQuery(cq) + .unwrap(Query.class) + .setParameter("arrayValues", new int[]{2, 3}, IntArrayType.INSTANCE) + .getSingleResult(); + + assertArrayEquals(new int[]{1, 2, 3}, event.getValues());*/ + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + private Long id; + + private String name; + + @Type(IntArrayType.class) + @Column( + name = "event_values", + columnDefinition = "integer[]" + ) + private int[] values; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int[] getValues() { + return values; + } + + public void setValues(int[] values) { + this.values = values; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/DefaultEmptyListArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/DefaultEmptyListArrayTypeTest.java new file mode 100644 index 000000000..c4e479d00 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/DefaultEmptyListArrayTypeTest.java @@ -0,0 +1,187 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class DefaultEmptyListArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + public void init() { + DataSource dataSource = newDataSource(); + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + try { + statement.executeUpdate( + "DROP TYPE sensor_state CASCADE" + ); + } catch (SQLException ignore) { + } + statement.executeUpdate( + "CREATE TYPE sensor_state AS ENUM ('ONLINE', 'OFFLINE', 'UNKNOWN')" + ); + statement.executeUpdate( + "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"" + ); + } catch (SQLException e) { + fail(e.getMessage()); + } + super.init(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return PostgreSQL95ArrayDialect.class.getName(); + } + }; + } + + @Test + public void testEmptyArrays() { + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals(new UUID[]{}, event.getSensorIds().toArray()); + assertArrayEquals(new String[]{}, event.getSensorNames().toArray()); + assertArrayEquals(new Integer[]{}, event.getSensorValues().toArray()); + assertArrayEquals(new Long[]{}, event.getSensorLongValues().toArray()); + assertArrayEquals(new SensorState[]{}, event.getSensorStates().toArray()); + assertArrayEquals(new Date[]{}, event.getDateValues().toArray()); + assertArrayEquals(new Date[]{}, event.getTimestampValues().toArray()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event extends BaseEntity { + @Type(ListArrayType.class) + @Column(name = "sensor_ids", columnDefinition = "uuid[]") + private List sensorIds = new ArrayList<>(); + + @Type(ListArrayType.class) + @Column(name = "sensor_names", columnDefinition = "text[]") + private List sensorNames = new ArrayList<>();; + + @Type(ListArrayType.class) + @Column(name = "sensor_values", columnDefinition = "integer[]") + private List sensorValues = new ArrayList<>();; + + @Type(ListArrayType.class) + @Column(name = "sensor_long_values", columnDefinition = "bigint[]") + private List sensorLongValues = new ArrayList<>();; + + @Type(ListArrayType.class) + @Column(name = "sensor_states", columnDefinition = "sensor_state[]") + private List sensorStates = new ArrayList<>();; + + @Type(ListArrayType.class) + @Column(name = "date_values", columnDefinition = "date[]") + private List dateValues = new ArrayList<>();; + + @Type(ListArrayType.class) + @Column(name = "timestamp_values", columnDefinition = "timestamp[]") + private List timestampValues = new ArrayList<>();; + + public List getSensorIds() { + return sensorIds; + } + + public void setSensorIds(List sensorIds) { + this.sensorIds = sensorIds; + } + + public List getSensorNames() { + return sensorNames; + } + + public void setSensorNames(List sensorNames) { + this.sensorNames = sensorNames; + } + + public List getSensorValues() { + return sensorValues; + } + + public void setSensorValues(List sensorValues) { + this.sensorValues = sensorValues; + } + + public List getSensorLongValues() { + return sensorLongValues; + } + + public void setSensorLongValues(List sensorLongValues) { + this.sensorLongValues = sensorLongValues; + } + + public List getSensorStates() { + return sensorStates; + } + + public void setSensorStates(List sensorStates) { + this.sensorStates = sensorStates; + } + + public List getDateValues() { + return dateValues; + } + + public void setDateValues(List dateValues) { + this.dateValues = dateValues; + } + + public List getTimestampValues() { + return timestampValues; + } + + public void setTimestampValues(List timestampValues) { + this.timestampValues = timestampValues; + } + } + + public enum SensorState { + ONLINE, OFFLINE, UNKNOWN; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/EhcacheListArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/EhcacheListArrayTypeTest.java new file mode 100644 index 000000000..f6f5138c3 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/EhcacheListArrayTypeTest.java @@ -0,0 +1,349 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.Cacheable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.*; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class EhcacheListArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + protected void additionalProperties(Properties properties) { + properties.setProperty("hibernate.cache.use_second_level_cache", "true"); + properties.setProperty("hibernate.cache.use_query_cache", "true"); + properties.setProperty("hibernate.cache.region.factory_class", "ehcache"); + } + + @Override + public void init() { + DataSource dataSource = newDataSource(); + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + try { + statement.executeUpdate( + "DROP TYPE sensor_state CASCADE" + ); + } catch (SQLException ignore) { + } + statement.executeUpdate( + "CREATE TYPE sensor_state AS ENUM ('ONLINE', 'OFFLINE', 'UNKNOWN')" + ); + statement.executeUpdate( + "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"" + ); + } catch (SQLException e) { + fail(e.getMessage()); + } + super.init(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return PostgreSQL95ArrayDialect.class.getName(); + } + }; + } + + @Test + public void test() { + + Date date1 = Date.from(LocalDate.of(1991, 12, 31).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + Date date2 = Date.from(LocalDate.of(1990, 1, 1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + event.setSensorIds(Arrays.asList(UUID.fromString("c65a3bcb-8b36-46d4-bddb-ae96ad016eb1"), UUID.fromString("72e95717-5294-4c15-aa64-a3631cf9a800"))); + event.setSensorNames(Arrays.asList("Temperature", "Pressure")); + event.setSensorValues(Arrays.asList(12, 756)); + event.setSensorLongValues(Arrays.asList(42L, 9223372036854775800L)); + event.setSensorStates(Arrays.asList(SensorState.ONLINE, SensorState.OFFLINE, SensorState.ONLINE, SensorState.UNKNOWN)); + event.setDateValues(Arrays.asList(date1, date2)); + event.setTimestampValues(Arrays.asList(date1, date2)); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals(new UUID[]{UUID.fromString("c65a3bcb-8b36-46d4-bddb-ae96ad016eb1"), UUID.fromString("72e95717-5294-4c15-aa64-a3631cf9a800")}, event.getSensorIds().toArray()); + assertArrayEquals(new String[]{"Temperature", "Pressure"}, event.getSensorNames().toArray()); + assertArrayEquals(new Integer[]{12, 756}, event.getSensorValues().toArray()); + assertArrayEquals(new Long[]{42L, 9223372036854775800L}, event.getSensorLongValues().toArray()); + assertArrayEquals(new SensorState[]{SensorState.ONLINE, SensorState.OFFLINE, SensorState.ONLINE, SensorState.UNKNOWN}, event.getSensorStates().toArray()); + assertArrayEquals(new Date[]{date1, date2}, event.getDateValues().toArray()); + assertArrayEquals(new Date[]{date1, date2}, event.getTimestampValues().toArray()); + }); + + //@Ignore("TODO: Unsupported YET!!!") + /*doInJPA(entityManager -> { + List events = entityManager.createNativeQuery( + "select " + + " id, " + + " sensor_ids, " + + " sensor_names, " + + " sensor_values " + + "from event ", Tuple.class) + .unwrap(org.hibernate.query.NativeQuery.class) + .addScalar("sensor_ids", UUIDArrayType.INSTANCE) + .addScalar("sensor_names", StringArrayType.INSTANCE) + .addScalar("sensor_values", IntArrayType.INSTANCE) + .getResultList(); + + assertEquals(2, events.size()); + });*/ + } + + @Test + public void testMixingNullValues() { + + Date date = Date.from(LocalDate.of(1990, 1, 1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + event.setSensorIds(Arrays.asList(null, UUID.fromString("72e95717-5294-4c15-aa64-a3631cf9a800"))); + event.setSensorNames(Arrays.asList("Temperature", null)); + event.setSensorValues(Arrays.asList(null, 756)); + event.setSensorLongValues(Arrays.asList(null, 9223372036854775800L)); + event.setSensorStates(Arrays.asList(null, SensorState.OFFLINE, SensorState.ONLINE, null)); + event.setDateValues(Arrays.asList(null, date)); + event.setTimestampValues(Arrays.asList(null, date)); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals(new UUID[]{null, UUID.fromString("72e95717-5294-4c15-aa64-a3631cf9a800")}, event.getSensorIds().toArray()); + assertArrayEquals(new String[]{"Temperature", null}, event.getSensorNames().toArray()); + assertArrayEquals(new Integer[]{null, 756}, event.getSensorValues().toArray()); + assertArrayEquals(new Long[]{null, 9223372036854775800L}, event.getSensorLongValues().toArray()); + assertArrayEquals(new SensorState[]{null, SensorState.OFFLINE, SensorState.ONLINE, null}, event.getSensorStates().toArray()); + assertArrayEquals(new Date[]{null, date}, event.getDateValues().toArray()); + assertArrayEquals(new Date[]{null, date}, event.getTimestampValues().toArray()); + }); + } + + @Test + public void testNullValues() { + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + event.setSensorIds(Arrays.asList(null, null)); + event.setSensorNames(Arrays.asList(null, null)); + event.setSensorValues(Arrays.asList(null, null)); + event.setSensorLongValues(Arrays.asList(null, null)); + event.setSensorStates(Arrays.asList(null, null)); + event.setDateValues(Arrays.asList(null, null)); + event.setTimestampValues(Arrays.asList(null, null)); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals(new UUID[]{null, null}, event.getSensorIds().toArray()); + assertArrayEquals(new String[]{null, null}, event.getSensorNames().toArray()); + assertArrayEquals(new Integer[]{null, null}, event.getSensorValues().toArray()); + assertArrayEquals(new Long[]{null, null}, event.getSensorLongValues().toArray()); + assertArrayEquals(new SensorState[]{null, null}, event.getSensorStates().toArray()); + assertArrayEquals(new Date[]{null, null}, event.getDateValues().toArray()); + assertArrayEquals(new Date[]{null, null}, event.getTimestampValues().toArray()); + }); + } + + @Test + public void testEmptyArrays() { + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + event.setSensorIds(Collections.emptyList()); + event.setSensorNames(Collections.emptyList()); + event.setSensorValues(Collections.emptyList()); + event.setSensorLongValues(Collections.emptyList()); + event.setSensorStates(Collections.emptyList()); + event.setDateValues(Collections.emptyList()); + event.setTimestampValues(Collections.emptyList()); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals(new UUID[]{}, event.getSensorIds().toArray()); + assertArrayEquals(new String[]{}, event.getSensorNames().toArray()); + assertArrayEquals(new Integer[]{}, event.getSensorValues().toArray()); + assertArrayEquals(new Long[]{}, event.getSensorLongValues().toArray()); + assertArrayEquals(new SensorState[]{}, event.getSensorStates().toArray()); + assertArrayEquals(new Date[]{}, event.getDateValues().toArray()); + assertArrayEquals(new Date[]{}, event.getTimestampValues().toArray()); + }); + } + + @Test + public void testNullArrays() { + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals(null, event.getSensorIds()); + assertEquals(null, event.getSensorNames()); + assertEquals(null, event.getSensorLongValues()); + assertEquals(null, event.getSensorStates()); + assertEquals(null, event.getDateValues()); + assertEquals(null, event.getTimestampValues()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + @Cacheable + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Event extends BaseEntity { + @Type(ListArrayType.class) + @Column(name = "sensor_ids", columnDefinition = "uuid[]") + private List sensorIds; + + @Type(ListArrayType.class) + @Column(name = "sensor_names", columnDefinition = "text[]") + private List sensorNames; + + @Type(ListArrayType.class) + @Column(name = "sensor_values", columnDefinition = "integer[]") + private List sensorValues; + + @Type(ListArrayType.class) + @Column(name = "sensor_long_values", columnDefinition = "bigint[]") + private List sensorLongValues; + + @Type(ListArrayType.class) + @Column(name = "sensor_states", columnDefinition = "sensor_state[]") + private List sensorStates; + + @Type(ListArrayType.class) + @Column(name = "date_values", columnDefinition = "date[]") + private List dateValues; + + @Type(ListArrayType.class) + @Column(name = "timestamp_values", columnDefinition = "timestamp[]") + private List timestampValues; + + public List getSensorIds() { + return sensorIds; + } + + public void setSensorIds(List sensorIds) { + this.sensorIds = sensorIds; + } + + public List getSensorNames() { + return sensorNames; + } + + public void setSensorNames(List sensorNames) { + this.sensorNames = sensorNames; + } + + public List getSensorValues() { + return sensorValues; + } + + public void setSensorValues(List sensorValues) { + this.sensorValues = sensorValues; + } + + public List getSensorLongValues() { + return sensorLongValues; + } + + public void setSensorLongValues(List sensorLongValues) { + this.sensorLongValues = sensorLongValues; + } + + public List getSensorStates() { + return sensorStates; + } + + public void setSensorStates(List sensorStates) { + this.sensorStates = sensorStates; + } + + public List getDateValues() { + return dateValues; + } + + public void setDateValues(List dateValues) { + this.dateValues = dateValues; + } + + public List getTimestampValues() { + return timestampValues; + } + + public void setTimestampValues(List timestampValues) { + this.timestampValues = timestampValues; + } + } + + public enum SensorState { + ONLINE, OFFLINE, UNKNOWN; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/HSQLDBArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/HSQLDBArrayTypeTest.java new file mode 100644 index 000000000..10cef485c --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/HSQLDBArrayTypeTest.java @@ -0,0 +1,174 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.util.AbstractTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; + +import static org.junit.Assert.assertArrayEquals; + +/** + * @author Vlad Mihalcea + */ +public class HSQLDBArrayTypeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Test + public void test() { + + Date date1 = Date.from(LocalDate.of(1991, 12, 31).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + Date date2 = Date.from(LocalDate.of(1990, 1, 1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + event.setSensorNames(new String[]{"Temperature", "Pressure"}); + event.setSensorValues(new int[]{12, 756}); + event.setSensorLongValues(new long[]{42L, 9223372036854775800L}); + event.setSensorDoubleValues(new double[]{0.123, 456.789}); + event.setDateValues(new Date[]{date1, date2}); + event.setTimestampValues(new Date[]{date1, date2}); + + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals(new String[]{"Temperature", "Pressure"}, event.getSensorNames()); + assertArrayEquals(new int[]{12, 756}, event.getSensorValues()); + assertArrayEquals(new long[]{42L, 9223372036854775800L}, event.getSensorLongValues()); + assertArrayEquals(new double[]{0.123, 456.789}, event.getSensorDoubleValues(), 0.01); + assertArrayEquals(new Date[]{date1, date2}, event.getDateValues()); + assertArrayEquals(new Date[]{date1, date2}, event.getTimestampValues()); + }); + + //@Ignore("TODO: Unsupported YET!!!") + /*doInJPA(entityManager -> { + List events = entityManager.createNativeQuery( + "select " + + " id, " + + " sensor_names, " + + " sensor_values, " + + " date_values " + + "from event ", Tuple.class) + .unwrap(org.hibernate.query.NativeQuery.class) + .addScalar("sensor_names", StringArrayType.INSTANCE) + .addScalar("sensor_values", IntArrayType.INSTANCE) + .addScalar("date_values", DateArrayType.INSTANCE) + .getResultList(); + + assertEquals(2, events.size()); + });*/ + } + + @Entity(name = "Event") + @Table(name = "event") + /*@TypeDefs({ + @TypeDef( + name = "hsqldb-double-array", + typeClass = DoubleArrayType.class, + parameters = { + @Parameter(name = EnumArrayType.SQL_ARRAY_TYPE, value = "double") + } + ), + @TypeDef( + name = "hsqldb-string-array", + typeClass = DoubleArrayType.class, + parameters = { + @Parameter(name = EnumArrayType.SQL_ARRAY_TYPE, value = "varchar") + } + ) + })*/ + public static class Event extends BaseEntity { + + @Type(DoubleArrayType.class) + @Column(name = "sensor_names", columnDefinition = "VARCHAR(20) ARRAY[10]") + private String[] sensorNames; + + @Type(IntArrayType.class) + @Column(name = "sensor_values", columnDefinition = "INT ARRAY DEFAULT ARRAY[]") + private int[] sensorValues; + + @Type(LongArrayType.class) + @Column(name = "sensor_long_values", columnDefinition = "BIGINT ARRAY DEFAULT ARRAY[]") + private long[] sensorLongValues; + + @Type(DoubleArrayType.class) + @Column(name = "sensor_double_values", columnDefinition = "DOUBLE ARRAY DEFAULT ARRAY[]") + private double[] sensorDoubleValues; + + @Type(DoubleArrayType.class) + @Column(name = "date_values", columnDefinition = "DATE ARRAY DEFAULT ARRAY[]") + private Date[] dateValues; + + @Type(TimestampArrayType.class) + @Column(name = "timestamp_values", columnDefinition = "TIMESTAMP ARRAY DEFAULT ARRAY[]") + private Date[] timestampValues; + + public String[] getSensorNames() { + return sensorNames; + } + + public void setSensorNames(String[] sensorNames) { + this.sensorNames = sensorNames; + } + + public int[] getSensorValues() { + return sensorValues; + } + + public void setSensorValues(int[] sensorValues) { + this.sensorValues = sensorValues; + } + + public long[] getSensorLongValues() { + return sensorLongValues; + } + + public void setSensorLongValues(long[] sensorLongValues) { + this.sensorLongValues = sensorLongValues; + } + + public double[] getSensorDoubleValues() { + return sensorDoubleValues; + } + + public void setSensorDoubleValues(double[] sensorDoubleValues) { + this.sensorDoubleValues = sensorDoubleValues; + } + + public Date[] getDateValues() { + return dateValues; + } + + public void setDateValues(Date[] dateValues) { + this.dateValues = dateValues; + } + + public Date[] getTimestampValues() { + return timestampValues; + } + + public void setTimestampValues(Date[] timestampValues) { + this.timestampValues = timestampValues; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/ListArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/ListArrayTypeTest.java new file mode 100644 index 000000000..07b58e22f --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/ListArrayTypeTest.java @@ -0,0 +1,633 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Parameter; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import javax.sql.DataSource; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class ListArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + public void init() { + DataSource dataSource = newDataSource(); + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + try { + statement.executeUpdate( + "DROP TYPE sensor_state CASCADE" + ); + } catch (SQLException ignore) { + } + statement.executeUpdate( + "CREATE TYPE sensor_state AS ENUM ('ONLINE', 'OFFLINE', 'UNKNOWN')" + ); + statement.executeUpdate( + "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"" + ); + } catch (SQLException e) { + fail(e.getMessage()); + } + super.init(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return PostgreSQL95ArrayDialect.class.getName(); + } + }; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + entityManager.persist( + new Event() + .setId(0L) + ); + + entityManager.persist( + new Event() + .setId(1L) + .setSensorIds( + Arrays.asList( + UUID.fromString("c65a3bcb-8b36-46d4-bddb-ae96ad016eb1"), + UUID.fromString("72e95717-5294-4c15-aa64-a3631cf9a800") + ) + ) + .setSensorNames(Arrays.asList("Temperature", "Pressure")) + .setSensorValues(Arrays.asList(12, 756)) + .setSensorLongValues(Arrays.asList(42L, 9223372036854775800L)) + .setSensorBooleanValues(Arrays.asList(true, false)) + .setSensorDoubleValues(Arrays.asList(0.123D, 456.789D)) + .setSensorStates( + Arrays.asList( + SensorState.ONLINE, SensorState.OFFLINE, + SensorState.ONLINE, SensorState.UNKNOWN + ) + ) + .setDateValues( + Arrays.asList( + java.sql.Date.valueOf(LocalDate.of(1991, 12, 31)), + java.sql.Date.valueOf(LocalDate.of(1990, 1, 1)) + ) + ) + .setTimestampValues( + Arrays.asList( + Date.from( + LocalDate.of(1991, 12, 31) + .atStartOfDay() + .atZone(ZoneId.systemDefault()) + .toInstant() + ), + Date.from( + LocalDate.of(1990, 1, 1) + .atStartOfDay() + .atZone(ZoneId.systemDefault()) + .toInstant() + ) + ) + ) + .setDecimalValues( + Arrays.asList( + BigDecimal.ONE, + BigDecimal.ZERO, + BigDecimal.TEN + ) + ) + .setLocalDateValues( + Arrays.asList( + LocalDate.of(2022, 3, 22), + LocalDate.of(2021, 4, 21) + ) + ) + .setLocalDateTimeValues( + Arrays.asList( + LocalDateTime.of(2022, 3, 22, 11, 22, 33), + LocalDateTime.of(2021, 4, 21, 22, 33, 44) + ) + ) + ); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals( + Arrays.asList( + UUID.fromString("c65a3bcb-8b36-46d4-bddb-ae96ad016eb1"), + UUID.fromString("72e95717-5294-4c15-aa64-a3631cf9a800") + ), + event.getSensorIds() + ); + assertEquals( + Arrays.asList("Temperature", "Pressure"), + event.getSensorNames() + ); + assertEquals( + Arrays.asList(12, 756), + event.getSensorValues() + ); + assertEquals( + Arrays.asList(42L, 9223372036854775800L), + event.getSensorLongValues() + ); + assertEquals( + Arrays.asList(true, false), + event.getSensorBooleanValues() + ); + assertEquals( + Arrays.asList(0.123D, 456.789D), + event.getSensorDoubleValues() + ); + assertEquals( + Arrays.asList( + SensorState.ONLINE, SensorState.OFFLINE, + SensorState.ONLINE, SensorState.UNKNOWN + ), + event.getSensorStates() + ); + assertEquals( + Arrays.asList( + java.sql.Date.valueOf(LocalDate.of(1991, 12, 31)), + java.sql.Date.valueOf(LocalDate.of(1990, 1, 1)) + ), + event.getDateValues() + ); + assertEquals( + Arrays.asList( + Date.from( + LocalDate.of(1991, 12, 31) + .atStartOfDay() + .atZone(ZoneId.systemDefault()) + .toInstant() + ), + Date.from( + LocalDate.of(1990, 1, 1) + .atStartOfDay() + .atZone(ZoneId.systemDefault()) + .toInstant() + ) + ), + event.getTimestampValues() + ); + assertEquals( + Arrays.asList( + BigDecimal.ONE, + BigDecimal.ZERO, + BigDecimal.TEN + ), + event.getDecimalValues() + ); + assertEquals( + Arrays.asList( + LocalDate.of(2022, 3, 22), + LocalDate.of(2021, 4, 21) + ), + event.getLocalDateValues() + ); + assertEquals( + Arrays.asList( + LocalDateTime.of(2022, 3, 22, 11, 22, 33), + LocalDateTime.of(2021, 4, 21, 22, 33, 44) + ), + event.getLocalDateTimeValues() + ); + }); + + //@Ignore("TODO: Unsupported YET!!!") + /*doInJPA(entityManager -> { + List events = entityManager.createNativeQuery( + "select " + + " id, " + + " sensor_ids, " + + " sensor_names, " + + " sensor_values, " + + " sensor_states " + + "from event ", Tuple.class) + .unwrap(org.hibernate.query.NativeQuery.class) + .addScalar("sensor_ids", UUIDArrayType.INSTANCE) + .addScalar("sensor_names", StringArrayType.INSTANCE) + .addScalar("sensor_values", IntArrayType.INSTANCE) + .addScalar( + "sensor_states", + new EnumArrayType( + SensorState[].class, + "sensor_state" + ) + ) + .getResultList(); + + assertEquals(2, events.size()); + });*/ + } + + @Test + public void testMixingNullValues() { + + Date date = Date.from(LocalDate.of(1990, 1, 1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + event.setSensorIds(Arrays.asList(null, UUID.fromString("72e95717-5294-4c15-aa64-a3631cf9a800"))); + event.setSensorNames(Arrays.asList("Temperature", null)); + event.setSensorValues(Arrays.asList(null, 756)); + event.setSensorLongValues(Arrays.asList(null, 9223372036854775800L)); + event.setSensorBooleanValues(Arrays.asList(null, false)); + event.setSensorDoubleValues(Arrays.asList(null, 456.789D)); + event.setSensorStates(Arrays.asList(null, SensorState.OFFLINE, SensorState.ONLINE, null)); + event.setDateValues(Arrays.asList(null, date)); + event.setTimestampValues(Arrays.asList(null, date)); + event.setDecimalValues(Arrays.asList(null, BigDecimal.TEN)); + event.setLocalDateValues(Arrays.asList(null, LocalDate.of(2021, 4, 21))); + event.setLocalDateTimeValues(Arrays.asList(null, LocalDateTime.of(2021, 4, 21, 22, 33, 44))); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals(new UUID[]{null, UUID.fromString("72e95717-5294-4c15-aa64-a3631cf9a800")}, event.getSensorIds().toArray()); + assertArrayEquals(new String[]{"Temperature", null}, event.getSensorNames().toArray()); + assertArrayEquals(new Integer[]{null, 756}, event.getSensorValues().toArray()); + assertArrayEquals(new Long[]{null, 9223372036854775800L}, event.getSensorLongValues().toArray()); + assertArrayEquals(new Boolean[]{null, false}, event.getSensorBooleanValues().toArray()); + assertArrayEquals(new Double[]{null, 456.789D}, event.getSensorDoubleValues().toArray()); + assertArrayEquals(new SensorState[]{null, SensorState.OFFLINE, SensorState.ONLINE, null}, event.getSensorStates().toArray()); + assertArrayEquals(new Date[]{null, date}, event.getDateValues().toArray()); + assertArrayEquals(new Date[]{null, date}, event.getTimestampValues().toArray()); + assertArrayEquals(new BigDecimal[]{null, BigDecimal.TEN}, event.getDecimalValues().toArray()); + assertArrayEquals(new LocalDate[]{null, LocalDate.of(2021, 4, 21)}, event.getLocalDateValues().toArray()); + assertArrayEquals(new LocalDateTime[]{null, LocalDateTime.of(2021, 4, 21, 22, 33, 44)}, event.getLocalDateTimeValues().toArray()); + }); + } + + @Test + public void testNullValues() { + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + event.setSensorIds(Arrays.asList(null, null)); + event.setSensorNames(Arrays.asList(null, null)); + event.setSensorValues(Arrays.asList(null, null)); + event.setSensorLongValues(Arrays.asList(null, null)); + event.setSensorBooleanValues(Arrays.asList(null, null)); + event.setSensorDoubleValues(Arrays.asList(null, null)); + event.setSensorStates(Arrays.asList(null, null)); + event.setDateValues(Arrays.asList(null, null)); + event.setTimestampValues(Arrays.asList(null, null)); + event.setDecimalValues(Arrays.asList(null, null)); + event.setLocalDateValues(Arrays.asList(null, null)); + event.setLocalDateTimeValues(Arrays.asList(null, null)); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals(new UUID[]{null, null}, event.getSensorIds().toArray()); + assertArrayEquals(new String[]{null, null}, event.getSensorNames().toArray()); + assertArrayEquals(new Integer[]{null, null}, event.getSensorValues().toArray()); + assertArrayEquals(new Long[]{null, null}, event.getSensorLongValues().toArray()); + assertArrayEquals(new Boolean[]{null, null}, event.getSensorBooleanValues().toArray()); + assertArrayEquals(new Double[]{null, null}, event.getSensorDoubleValues().toArray()); + assertArrayEquals(new SensorState[]{null, null}, event.getSensorStates().toArray()); + assertArrayEquals(new Date[]{null, null}, event.getDateValues().toArray()); + assertArrayEquals(new Date[]{null, null}, event.getTimestampValues().toArray()); + assertArrayEquals(new BigDecimal[]{null, null}, event.getDecimalValues().toArray()); + assertArrayEquals(new LocalDate[]{null, null}, event.getLocalDateValues().toArray()); + assertArrayEquals(new LocalDateTime[]{null, null}, event.getLocalDateTimeValues().toArray()); + }); + } + + @Test + public void testEmptyArrays() { + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + event.setSensorIds(Collections.emptyList()); + event.setSensorNames(Collections.emptyList()); + event.setSensorValues(Collections.emptyList()); + event.setSensorLongValues(Collections.emptyList()); + event.setSensorBooleanValues(Collections.emptyList()); + event.setSensorDoubleValues(Collections.emptyList()); + event.setSensorStates(Collections.emptyList()); + event.setDateValues(Collections.emptyList()); + event.setTimestampValues(Collections.emptyList()); + event.setDecimalValues(Collections.emptyList()); + event.setLocalDateValues(Collections.emptyList()); + event.setLocalDateTimeValues(Collections.emptyList()); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals(new UUID[]{}, event.getSensorIds().toArray()); + assertArrayEquals(new String[]{}, event.getSensorNames().toArray()); + assertArrayEquals(new Integer[]{}, event.getSensorValues().toArray()); + assertArrayEquals(new Long[]{}, event.getSensorLongValues().toArray()); + assertArrayEquals(new Boolean[]{}, event.getSensorBooleanValues().toArray()); + assertArrayEquals(new Double[]{}, event.getSensorDoubleValues().toArray()); + assertArrayEquals(new SensorState[]{}, event.getSensorStates().toArray()); + assertArrayEquals(new Date[]{}, event.getDateValues().toArray()); + assertArrayEquals(new Date[]{}, event.getTimestampValues().toArray()); + assertArrayEquals(new BigDecimal[]{}, event.getDecimalValues().toArray()); + assertArrayEquals(new LocalDate[]{}, event.getLocalDateValues().toArray()); + assertArrayEquals(new LocalDateTime[]{}, event.getLocalDateTimeValues().toArray()); + }); + } + + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + private Long id; + + @Type(ListArrayType.class) + @Column( + name = "sensor_ids", + columnDefinition = "uuid[]" + ) + private List sensorIds; + + @Type(ListArrayType.class) + @Column( + name = "sensor_names", + columnDefinition = "text[]" + ) + private List sensorNames; + + @Type(ListArrayType.class) + @Column( + name = "sensor_values", + columnDefinition = "integer[]" + ) + private List sensorValues; + + @Type(ListArrayType.class) + @Column( + name = "sensor_long_values", + columnDefinition = "bigint[]" + ) + private List sensorLongValues; + + @Type(ListArrayType.class) + @Column( + name = "sensor_boolean_values", + columnDefinition = "boolean[]" + ) + private List sensorBooleanValues; + + @Type(ListArrayType.class) + @Column( + name = "sensor_double_values", + columnDefinition = "float8[]" + ) + private List sensorDoubleValues; + + @Type( + value = ListArrayType.class, + parameters = { + @Parameter( + name = ListArrayType.SQL_ARRAY_TYPE, + value = "sensor_state" + ) + } + ) + @Column( + name = "sensor_states", + columnDefinition = "sensor_state[]" + ) + private List sensorStates; + + @Type(ListArrayType.class) + @Column( + name = "date_values", + columnDefinition = "date[]" + ) + private List dateValues; + + @Type(ListArrayType.class) + @Column( + name = "timestamp_values", + columnDefinition = "timestamp[]" + ) + private List timestampValues; + + @Type(ListArrayType.class) + @Column( + name = "decimal_values", + columnDefinition = "decimal[]" + ) + private List decimalValues; + + @Type(ListArrayType.class) + @Column( + name = "localdate_values", + columnDefinition = "date[]" + ) + private List localDateValues; + + @Type(ListArrayType.class) + @Column( + name = "localdatetime_values", + columnDefinition = "timestamp[]" + ) + private List localDateTimeValues; + + public Long getId() { + return id; + } + + public Event setId(Long id) { + this.id = id; + return this; + } + + public List getSensorIds() { + return sensorIds; + } + + public Event setSensorIds(List sensorIds) { + this.sensorIds = sensorIds; + return this; + } + + public List getSensorNames() { + return sensorNames; + } + + public Event setSensorNames(List sensorNames) { + this.sensorNames = sensorNames; + return this; + } + + public List getSensorValues() { + return sensorValues; + } + + public Event setSensorValues(List sensorValues) { + this.sensorValues = sensorValues; + return this; + } + + public List getSensorLongValues() { + return sensorLongValues; + } + + public Event setSensorLongValues(List sensorLongValues) { + this.sensorLongValues = sensorLongValues; + return this; + } + + public List getSensorBooleanValues() { + return sensorBooleanValues; + } + + public Event setSensorBooleanValues(List sensorBooleanValues) { + this.sensorBooleanValues = sensorBooleanValues; + return this; + } + + public List getSensorDoubleValues() { + return sensorDoubleValues; + } + + public Event setSensorDoubleValues(List sensorDoubleValues) { + this.sensorDoubleValues = sensorDoubleValues; + return this; + } + + public List getSensorStates() { + return sensorStates; + } + + public Event setSensorStates(List sensorStates) { + this.sensorStates = sensorStates; + return this; + } + + public List getDateValues() { + return dateValues; + } + + public Event setDateValues(List dateValues) { + this.dateValues = dateValues; + return this; + } + + public List getTimestampValues() { + return timestampValues; + } + + public Event setTimestampValues(List timestampValues) { + this.timestampValues = timestampValues; + return this; + } + + public List getDecimalValues() { + return decimalValues; + } + + public Event setDecimalValues(List decimalValues) { + this.decimalValues = decimalValues; + return this; + } + + public List getLocalDateValues() { + return localDateValues; + } + + public Event setLocalDateValues(List localDateValues) { + this.localDateValues = localDateValues; + return this; + } + + public List getLocalDateTimeValues() { + return localDateTimeValues; + } + + public Event setLocalDateTimeValues(List localDateTimeValues) { + this.localDateTimeValues = localDateTimeValues; + return this; + } + } + + @Test + public void testNullArrays() { + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals(null, event.getSensorIds()); + assertEquals(null, event.getSensorNames()); + assertEquals(null, event.getSensorLongValues()); + assertEquals(null, event.getSensorStates()); + assertEquals(null, event.getDateValues()); + assertEquals(null, event.getTimestampValues()); + }); + } + + public enum SensorState { + ONLINE, OFFLINE, UNKNOWN; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MappedSuperclassListArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MappedSuperclassListArrayTypeTest.java new file mode 100644 index 000000000..776697b38 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MappedSuperclassListArrayTypeTest.java @@ -0,0 +1,104 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.*; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; + +/** + * @author Vlad Mihalcea + */ +public class MappedSuperclassListArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return PostgreSQL95ArrayDialect.class.getName(); + } + }; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + event.setSensorNames(Arrays.asList("Temperature", "Pressure")); + event.setSensorValues(Arrays.asList(12, 756)); + + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals(new String[]{"Temperature", "Pressure"}, event.getSensorNames().toArray()); + assertArrayEquals(new Integer[]{12, 756}, event.getSensorValues().toArray()); + }); + } + + @MappedSuperclass + public static class BaseEntity { + + @Id + private Long id; + + @Type(ListArrayType.class) + @Column(name = "sensor_names", columnDefinition = "text[]") + private List sensorNames; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public List getSensorNames() { + return sensorNames; + } + + public void setSensorNames(List sensorNames) { + this.sensorNames = sensorNames; + } + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event extends BaseEntity { + + @Type(ListArrayType.class) + @Column(name = "sensor_values", columnDefinition = "integer[]") + private List sensorValues; + + public List getSensorValues() { + return sensorValues; + } + + public void setSensorValues(List sensorValues) { + this.sensorValues = sensorValues; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MatrixArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MatrixArrayTypeTest.java new file mode 100644 index 000000000..0acd47b9a --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MatrixArrayTypeTest.java @@ -0,0 +1,118 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; + +/** + * @author Vlad Mihalcea + */ +public class MatrixArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return PostgreSQL95ArrayDialect.class.getName(); + } + }; + } + + @Test + public void testNonEmptyArray() { + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + entityManager.persist( + new Event() + .setId(1L) + .setMatrix(new String[][]{ + {"A", "B", "C"}, + {"1", "2", "3"}, + }) + ); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + assertArrayEquals( + new String[][]{ + {"A", "B", "C"}, + {"1", "2", "3"}, + }, + event.getMatrix() + ); + }); + } + + @Test + public void testEmptyArray() { + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + entityManager.persist( + new Event() + .setId(1L) + .setMatrix(new String[][]{}) + ); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + assertArrayEquals( + new String[][]{}, + event.getMatrix() + ); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + private Long id; + + @Type(StringArrayType.class) + @Column(name = "test_2d_array", columnDefinition = "text[]") + private String[][] matrix; + + public Long getId() { + return id; + } + + public Event setId(Long id) { + this.id = id; + return this; + } + + public String[][] getMatrix() { + return matrix; + } + + public Event setMatrix(String[][] matrix) { + this.matrix = matrix; + return this; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MultiDimensionalArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MultiDimensionalArrayTypeTest.java new file mode 100644 index 000000000..2f64b0b58 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MultiDimensionalArrayTypeTest.java @@ -0,0 +1,173 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Before; +import org.junit.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class MultiDimensionalArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Plane.class, + }; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return PostgreSQL95ArrayDialect.class.getName(); + } + }; + } + + @Before + public void init() { + DataSource dataSource = newDataSource(); + try (Connection connection = dataSource.getConnection()) { + try (Statement statement = connection.createStatement()){ + statement.executeUpdate("DROP TABLE IF EXISTS plane;"); + statement.executeUpdate("DROP TYPE IF EXISTS seat_status CASCADE;"); + statement.executeUpdate("CREATE TYPE seat_status AS ENUM ('UNRESERVED', 'RESERVED', 'BLOCKED');"); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + super.init(); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Plane() + .setId(1L) + .setName("ATR-42") + .setSeatGrid( + new SeatStatus[][] { + {SeatStatus.BLOCKED, SeatStatus.BLOCKED, SeatStatus.BLOCKED, SeatStatus.BLOCKED}, + {SeatStatus.UNRESERVED, SeatStatus.UNRESERVED, SeatStatus.RESERVED, SeatStatus.UNRESERVED}, + {SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED}, + {SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED}, + {SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED}, + {SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED}, + {SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED}, + {SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED}, + {SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED}, + {SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED}, + {SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED, SeatStatus.RESERVED}, + {SeatStatus.BLOCKED, SeatStatus.BLOCKED, SeatStatus.BLOCKED, SeatStatus.BLOCKED} + } + ) + ); + }); + + doInJPA(entityManager -> { + Plane plane = entityManager.find(Plane.class, 1L); + + assertEquals("ATR-42", plane.getName()); + assertEquals(SeatStatus.BLOCKED, plane.getSeatStatus(1, 'A')); + assertEquals(SeatStatus.BLOCKED, plane.getSeatStatus(1, 'B')); + assertEquals(SeatStatus.BLOCKED, plane.getSeatStatus(1, 'C')); + assertEquals(SeatStatus.BLOCKED, plane.getSeatStatus(1, 'D')); + assertEquals(SeatStatus.UNRESERVED, plane.getSeatStatus(2, 'A')); + assertEquals(SeatStatus.UNRESERVED, plane.getSeatStatus(2, 'B')); + assertEquals(SeatStatus.RESERVED, plane.getSeatStatus(2, 'C')); + assertEquals(SeatStatus.UNRESERVED, plane.getSeatStatus(2, 'D')); + }); + + //@Ignore("TODO: Unsupported YET!!!") + /*doInJPA(entityManager -> { + List tuples = entityManager + .createNativeQuery( + "SELECT * " + + " FROM plane ", Tuple.class) + .unwrap(org.hibernate.query.NativeQuery.class) + .addScalar( + "seat_grid", + new EnumArrayType( + (Class) ReflectionUtils.getField(Plane.class, "seatGrid").getType(), + "seat_status_array" + ) + ) + .addScalar("name", StringType.INSTANCE) + .addScalar("id", LongType.INSTANCE) + .getResultList(); + + Tuple plane = tuples.get(0); + assertEquals("ATR-42", plane.get("name")); + });*/ + } + + public enum SeatStatus { + UNRESERVED, + RESERVED, + BLOCKED, + } + + @Entity(name = "Plane") + @Table(name = "plane") + public static class Plane { + + @Id + private Long id; + + private String name; + + @Type(EnumArrayType.class) + @Column(name = "seat_grid", columnDefinition = "seat_status[][]") + private SeatStatus[][] seatGrid; + + public Long getId() { + return id; + } + + public Plane setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Plane setName(String name) { + this.name = name; + return this; + } + + public SeatStatus[][] getSeatGrid() { + return seatGrid; + } + + public Plane setSeatGrid(SeatStatus[][] seatGrid) { + this.seatGrid = seatGrid; + return this; + } + + public SeatStatus getSeatStatus(int row, char letter) { + return seatGrid[row - 1][letter - 65]; + } + } + +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MultiDimensionalIntegerArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MultiDimensionalIntegerArrayTypeTest.java new file mode 100644 index 000000000..480d9b2d7 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MultiDimensionalIntegerArrayTypeTest.java @@ -0,0 +1,144 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MultiDimensionalIntegerArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Plane.class, + }; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return PostgreSQL95ArrayDialect.class.getName(); + } + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Plane() + .setId(1L) + .setName("ATR-42") + .setSeatGrid( + new Integer[][] { + {1, 1, 1, 1}, + {0, 0, 2, 0}, + {2, 2, 2, 2}, + {2, 2, 2, 2}, + {2, 2, 2, 2}, + {2, 2, 2, 2}, + {2, 2, 2, 2}, + {2, 2, 2, 2}, + {2, 2, 2, 2}, + {2, 2, 2, 2}, + {2, 2, 2, 2}, + {1, 1, 1, 1} + } + ) + ); + }); + + doInJPA(entityManager -> { + Plane plane = entityManager.find(Plane.class, 1L); + + assertEquals("ATR-42", plane.getName()); + assertEquals(1, plane.getSeatStatus(1, 'A')); + assertEquals(1, plane.getSeatStatus(1, 'B')); + assertEquals(1, plane.getSeatStatus(1, 'C')); + assertEquals(1, plane.getSeatStatus(1, 'D')); + assertEquals(0, plane.getSeatStatus(2, 'A')); + assertEquals(0, plane.getSeatStatus(2, 'B')); + assertEquals(2, plane.getSeatStatus(2, 'C')); + assertEquals(0, plane.getSeatStatus(2, 'D')); + }); + + //@Ignore("TODO: Unsupported YET!!!") + /*doInJPA(entityManager -> { + List tuples = entityManager + .createNativeQuery( + "SELECT * " + + " FROM plane ", Tuple.class) + .unwrap(org.hibernate.query.NativeQuery.class) + .addScalar( + "seat_grid", + new IntArrayType( + ReflectionUtils.getField(Plane.class, "seatGrid").getType() + ) + ) + .addScalar("name", StringType.INSTANCE) + .addScalar("id", LongType.INSTANCE) + .getResultList(); + + Tuple plane = tuples.get(0); + assertEquals("ATR-42", plane.get("name")); + });*/ + } + + @Entity(name = "Plane") + @Table(name = "plane") + public static class Plane { + + @Id + private Long id; + + private String name; + + @Type(IntArrayType.class) + @Column(name = "seat_grid", columnDefinition = "int[][]") + private Integer[][] seatGrid; + + public Long getId() { + return id; + } + + public Plane setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Plane setName(String name) { + this.name = name; + return this; + } + + public Integer[][] getSeatGrid() { + return seatGrid; + } + + public Plane setSeatGrid(Integer[][] seatGrid) { + this.seatGrid = seatGrid; + return this; + } + + public int getSeatStatus(int row, char letter) { + return seatGrid[row - 1][letter - 65]; + } + } + +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MultiDimensionalStringArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MultiDimensionalStringArrayTypeTest.java new file mode 100644 index 000000000..f3bfeaf69 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/MultiDimensionalStringArrayTypeTest.java @@ -0,0 +1,144 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MultiDimensionalStringArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Plane.class, + }; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return PostgreSQL95ArrayDialect.class.getName(); + } + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Plane() + .setId(1L) + .setName("ATR-42") + .setSeatGrid( + new String[][] { + {"BLOCKED", "BLOCKED", "BLOCKED", "BLOCKED"}, + {"UNRESERVED", "UNRESERVED", "RESERVED", "UNRESERVED"}, + {"RESERVED", "RESERVED", "RESERVED", "RESERVED"}, + {"RESERVED", "RESERVED", "RESERVED", "RESERVED"}, + {"RESERVED", "RESERVED", "RESERVED", "RESERVED"}, + {"RESERVED", "RESERVED", "RESERVED", "RESERVED"}, + {"RESERVED", "RESERVED", "RESERVED", "RESERVED"}, + {"RESERVED", "RESERVED", "RESERVED", "RESERVED"}, + {"RESERVED", "RESERVED", "RESERVED", "RESERVED"}, + {"RESERVED", "RESERVED", "RESERVED", "RESERVED"}, + {"RESERVED", "RESERVED", "RESERVED", "RESERVED"}, + {"BLOCKED", "BLOCKED", "BLOCKED", "BLOCKED"} + } + ) + ); + }); + + doInJPA(entityManager -> { + Plane plane = entityManager.find(Plane.class, 1L); + + assertEquals("ATR-42", plane.getName()); + assertEquals("BLOCKED", plane.getSeatStatus(1, 'A')); + assertEquals("BLOCKED", plane.getSeatStatus(1, 'B')); + assertEquals("BLOCKED", plane.getSeatStatus(1, 'C')); + assertEquals("BLOCKED", plane.getSeatStatus(1, 'D')); + assertEquals("UNRESERVED", plane.getSeatStatus(2, 'A')); + assertEquals("UNRESERVED", plane.getSeatStatus(2, 'B')); + assertEquals("RESERVED", plane.getSeatStatus(2, 'C')); + assertEquals("UNRESERVED", plane.getSeatStatus(2, 'D')); + }); + + //@Ignore("TODO: Unsupported YET!!!") + /*doInJPA(entityManager -> { + List tuples = entityManager + .createNativeQuery( + "SELECT * " + + " FROM plane ", Tuple.class) + .unwrap(org.hibernate.query.NativeQuery.class) + .addScalar( + "seat_grid", + new StringArrayType( + ReflectionUtils.getField(Plane.class, "seatGrid").getType() + ) + ) + .addScalar("name", StringType.INSTANCE) + .addScalar("id", LongType.INSTANCE) + .getResultList(); + + Tuple plane = tuples.get(0); + assertEquals("ATR-42", plane.get("name")); + });*/ + } + + @Entity(name = "Plane") + @Table(name = "plane") + public static class Plane { + + @Id + private Long id; + + private String name; + + @Type(StringArrayType.class) + @Column(name = "seat_grid", columnDefinition = "text[][]") + private String[][] seatGrid; + + public Long getId() { + return id; + } + + public Plane setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Plane setName(String name) { + this.name = name; + return this; + } + + public String[][] getSeatGrid() { + return seatGrid; + } + + public Plane setSeatGrid(String[][] seatGrid) { + this.seatGrid = seatGrid; + return this; + } + + public String getSeatStatus(int row, char letter) { + return seatGrid[row - 1][letter - 65]; + } + } + +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/PostgreSQL95ArrayDialect.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/PostgreSQL95ArrayDialect.java new file mode 100644 index 000000000..c9a00d276 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/PostgreSQL95ArrayDialect.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hibernate.type.array; + +import org.hibernate.dialect.PostgreSQL95Dialect; + +import java.sql.Types; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQL95ArrayDialect extends PostgreSQL95Dialect { + + public PostgreSQL95ArrayDialect() { + super(); + //this.registerColumnType(Types.ARRAY, "array"); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/PostgreSQLEnumArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/PostgreSQLEnumArrayTypeTest.java new file mode 100644 index 000000000..dec6d1570 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/PostgreSQLEnumArrayTypeTest.java @@ -0,0 +1,164 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.annotations.Type; +import org.hibernate.query.Query; +import org.hibernate.query.TypedParameterValue; +import org.junit.Before; +import org.junit.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; + +/** + * @author Nikita Konev + */ +public class PostgreSQLEnumArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + public static final EnumArrayType ROLE_TYPE = new EnumArrayType(UserRole.class, "user_role"); + + @Override + protected Class[] entities() { + return new Class[]{ + UserAccount.class + }; + } + + @Before + public void init() { + DataSource dataSource = newDataSource(); + try (Connection connection = dataSource.getConnection()) { + try (Statement statement = connection.createStatement()){ + statement.executeUpdate("DROP TYPE IF EXISTS user_role;"); + statement.executeUpdate("CREATE TYPE user_role AS ENUM ('ROLE_ADMIN', 'ROLE_USER');"); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + super.init(); + } + + @Test + public void test() { + UserRole[] userRoles = {UserRole.ROLE_ADMIN, UserRole.ROLE_USER}; + + doInJPA(entityManager -> { + UserAccount account = new UserAccount(); + account.setUsername("vladmihalcea.com"); + account.setRoles(userRoles); + entityManager.persist(account); + }); + + doInJPA(entityManager -> { + UserAccount singleResult = entityManager + .createQuery( + "select ua " + + "from UserAccountEntity ua " + + "where ua.username = :username", UserAccount.class) + .setParameter("username", "vladmihalcea.com") + .getSingleResult(); + + assertArrayEquals(userRoles, singleResult.getRoles()); + }); + } + + @Test + public void testSetParameterWithType() { + UserRole[] userRoles = {UserRole.ROLE_ADMIN, UserRole.ROLE_USER}; + UserRole[] requiredRoles = {UserRole.ROLE_USER}; + + doInJPA(entityManager -> { + UserAccount account = new UserAccount(); + account.setUsername("vladmihalcea.com"); + account.setRoles(userRoles); + entityManager.persist(account); + }); + + doInJPA(entityManager -> { + entityManager + .createQuery( + "select ua " + + "from UserAccountEntity ua " + + "where ua.roles = :roles", UserAccount.class) + .unwrap(Query.class) + .setParameter("roles", requiredRoles, new EnumArrayType(UserRole.class, "user_role")) + .getResultList(); + }); + } + + @Test + public void testTypedParameterValue() { + UserRole[] userRoles = {UserRole.ROLE_ADMIN, UserRole.ROLE_USER}; + UserRole[] requiredRoles = {UserRole.ROLE_USER}; + + doInJPA(entityManager -> { + UserAccount account = new UserAccount(); + account.setUsername("vladmihalcea.com"); + account.setRoles(userRoles); + entityManager.persist(account); + }); + + doInJPA(entityManager -> { + entityManager + .createQuery( + "select ua " + + "from UserAccountEntity ua " + + "where ua.roles = :roles", UserAccount.class) + .setParameter("roles", new TypedParameterValue(ROLE_TYPE, requiredRoles)) + .getResultList(); + }); + } + + public enum UserRole { + ROLE_ADMIN, + ROLE_USER, + } + + @Entity(name = "UserAccountEntity") + @Table(name = "users") + public static class UserAccount { + + @Id + @GeneratedValue + private Long id; + + private String username; + + @Type(EnumArrayType.class) + @Column( + name = "roles", + columnDefinition = "user_role[]" + ) + private UserRole roles[]; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public UserRole[] getRoles() { + return roles; + } + + public void setRoles(UserRole[] roles) { + this.roles = roles; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/PostgreSQLMultipleEnumArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/PostgreSQLMultipleEnumArrayTypeTest.java new file mode 100644 index 000000000..1010c33d9 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/PostgreSQLMultipleEnumArrayTypeTest.java @@ -0,0 +1,158 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.annotations.Type; +import org.junit.Before; +import org.junit.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLMultipleEnumArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + UserAccount.class + }; + } + + @Before + public void init() { + DataSource dataSource = newDataSource(); + + try (Connection connection = dataSource.getConnection()) { + try (Statement statement = connection.createStatement()){ + statement.executeUpdate("DROP TYPE IF EXISTS user_role;"); + statement.executeUpdate("CREATE TYPE user_role AS ENUM ('ROLE_ADMIN', 'ROLE_USER');"); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + + try (Connection connection = dataSource.getConnection()) { + try (Statement statement = connection.createStatement()){ + statement.executeUpdate("DROP TYPE IF EXISTS user_role;"); + statement.executeUpdate("CREATE TYPE user_role AS ENUM ('ROLE_ADMIN', 'ROLE_USER');"); + } + + try (Statement statement = connection.createStatement()){ + statement.executeUpdate("DROP TYPE IF EXISTS user_type;"); + statement.executeUpdate("CREATE TYPE user_type AS ENUM ('SUPER_USER', 'REGULAR');"); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + super.init(); + } + + @Test + public void test() { + final UserRole[] userRoles = { + UserRole.ROLE_ADMIN, + UserRole.ROLE_USER + }; + + final UserType[] userTypes = { + UserType.SUPER_USER, + UserType.REGULAR + }; + + doInJPA(entityManager -> { + UserAccount account = new UserAccount(); + account.setUsername("vladmihalcea.com"); + account.setRoles(userRoles); + account.setTypes(userTypes); + entityManager.persist(account); + }); + + doInJPA(entityManager -> { + UserAccount singleResult = entityManager + .createQuery( + "select ua " + + "from UserAccountEntity ua " + + "where ua.username = :username", UserAccount.class) + .setParameter("username", "vladmihalcea.com") + .getSingleResult(); + + assertArrayEquals(userRoles, singleResult.getRoles()); + assertArrayEquals(userTypes, singleResult.getTypes()); + }); + } + + public enum UserRole { + ROLE_ADMIN, + ROLE_USER, + } + + public enum UserType { + SUPER_USER, + REGULAR, + } + + @Entity(name = "UserAccountEntity") + @Table(name = "users") + public static class UserAccount { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String username; + + @Type(EnumArrayType.class) + @Column( + name = "roles", + columnDefinition = "user_role[]" + ) + private UserRole roles[]; + + @Type(EnumArrayType.class) + @Column( + name = "types", + columnDefinition = "user_type[]" + ) + private UserType types[]; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public UserRole[] getRoles() { + return roles; + } + + public void setRoles(UserRole[] roles) { + this.roles = roles; + } + + public UserType[] getTypes() { + return types; + } + + public void setTypes(UserType[] types) { + this.types = types; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/SimpleArrayTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/SimpleArrayTypeTest.java new file mode 100644 index 000000000..7850d672b --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/array/SimpleArrayTypeTest.java @@ -0,0 +1,156 @@ +package com.vladmihalcea.hibernate.type.array; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class SimpleArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + public void init() { + DataSource dataSource = newDataSource(); + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + try { + statement.executeUpdate( + "DROP TYPE sensor_state CASCADE" + ); + } catch (SQLException ignore) { + } + statement.executeUpdate( + "CREATE TYPE sensor_state AS ENUM ('ONLINE', 'OFFLINE', 'UNKNOWN')" + ); + } catch (SQLException e) { + fail(e.getMessage()); + } + super.init(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return PostgreSQL95ArrayDialect.class.getName(); + } + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + entityManager.persist( + new Event() + .setId(1L) + .setSensorNames(new String[]{"Temperature", "Pressure"}) + .setSensorValues(new int[]{12, 756}) + .setSensorStates(new SensorState[]{SensorState.ONLINE, SensorState.OFFLINE, SensorState.ONLINE, SensorState.UNKNOWN}) + ); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals(new String[]{"Temperature", "Pressure"}, event.getSensorNames()); + assertArrayEquals(new int[]{12, 756}, event.getSensorValues()); + assertArrayEquals(new SensorState[]{SensorState.ONLINE, SensorState.OFFLINE, SensorState.ONLINE, SensorState.UNKNOWN}, event.getSensorStates()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + private Long id; + + @Type(StringArrayType.class) + @Column( + name = "sensor_names", + columnDefinition = "text[]" + ) + private String[] sensorNames; + + @Type(IntArrayType.class) + @Column( + name = "sensor_values", + columnDefinition = "integer[]" + ) + private int[] sensorValues; + + @Type(EnumArrayType.class) + @Column( + name = "sensor_states", + columnDefinition = "sensor_state[]" + ) + private SensorState[] sensorStates; + + public Long getId() { + return id; + } + + public Event setId(Long id) { + this.id = id; + return this; + } + + public String[] getSensorNames() { + return sensorNames; + } + + public Event setSensorNames(String[] sensorNames) { + this.sensorNames = sensorNames; + return this; + } + + public int[] getSensorValues() { + return sensorValues; + } + + public Event setSensorValues(int[] sensorValues) { + this.sensorValues = sensorValues; + return this; + } + + public SensorState[] getSensorStates() { + return sensorStates; + } + + public Event setSensorStates(SensorState[] sensorStates) { + this.sensorStates = sensorStates; + return this; + } + } + + public enum SensorState { + ONLINE, OFFLINE, UNKNOWN; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/Iso8601MonthTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/Iso8601MonthTest.java new file mode 100644 index 000000000..1f14eda78 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/Iso8601MonthTest.java @@ -0,0 +1,124 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Assert; +import org.junit.Test; + +import java.time.Month; +import java.time.Year; + +import static org.junit.Assert.assertEquals; + +/** + * @author Martin Panzer + */ +public class Iso8601MonthTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Publisher.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Publisher publisher = new Publisher(); + publisher.setName("vladmihalcea.com"); + publisher.setEstYear(Year.of(2013)); + publisher.setSalesMonth(Month.NOVEMBER); + + entityManager.persist(publisher); + }); + + doInJPA(entityManager -> { + Publisher publisher = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Publisher.class) + .load("vladmihalcea.com"); + + assertEquals(Year.of(2013), publisher.getEstYear()); + assertEquals(Month.NOVEMBER, publisher.getSalesMonth()); + }); + + doInJPA(entityManager -> { + Publisher book = entityManager + .createQuery( + "select p " + + "from Publisher p " + + "where " + + " p.estYear = :estYear and " + + " p.salesMonth = :salesMonth", Publisher.class) + .setParameter("estYear", Year.of(2013)) + .setParameter("salesMonth", Month.NOVEMBER) + .getSingleResult(); + + assertEquals("vladmihalcea.com", book.getName()); + }); + + doInJPA(entityManager -> { + Query query = entityManager.createNativeQuery("Select p.sales_month from publisher p where p.name = :name"); + query.setParameter("name", "vladmihalcea.com"); + Short result = (Short)query.getSingleResult(); + + Assert.assertEquals(11L, result.longValue()); + }); + } + + + @Entity(name = "Publisher") + @Table(name = "publisher") + public static class Publisher { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @Column(name = "est_year") + private Year estYear; + + @Type(Iso8601MonthType.class) + @Column(name = "sales_month", columnDefinition = "smallint") + private Month salesMonth; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Year getEstYear() { + return estYear; + } + + public void setEstYear(Year estYear) { + this.estYear = estYear; + } + + public Month getSalesMonth() { + return salesMonth; + } + + public void setSalesMonth(Month salesMonth) { + this.salesMonth = salesMonth; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/MySQLMonthDayDateTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/MySQLMonthDayDateTest.java new file mode 100644 index 000000000..e14dc74e0 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/MySQLMonthDayDateTest.java @@ -0,0 +1,153 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.MonthDay; +import java.util.ArrayList; +import java.util.TimeZone; + +import static org.junit.Assert.assertEquals; + +/** + * @author Mladen Savic (mladensavic94@gmail.com) + */ +public class MySQLMonthDayDateTest extends AbstractMySQLIntegrationTest { + + public static final String COLUMN_TYPE = "date"; + + @Override + protected Class[] entities() { + return new Class[]{Season.class}; + } + + private TimeZone defaultTimeZone; + + @Override + protected void afterInit() { + defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Athens")); + } + + @Override + public void destroy() { + super.destroy(); + TimeZone.setDefault(defaultTimeZone); + } + + @Test + public void testCreationAndFetchById(){ + Season season = createEntity("Summer", MonthDay.of(6,21), MonthDay.of(9,22)); + + doInJPA(entityManager -> { + Season summer = entityManager.unwrap(Session.class).find(Season.class, season.getId()); + + assertEquals(summer.getBeginningOfSeason(), MonthDay.of(6,21)); + assertEquals(summer.getEndOfSeason(), MonthDay.of(9,22)); + }); + + assertEquals(COLUMN_TYPE, getColumnType("end_of_season") ); + assertEquals(COLUMN_TYPE, getColumnType("beginning_of_season") ); + + } + + @Test + public void testFetchWithQuery(){ + createEntity("Winter", MonthDay.of(12,21), MonthDay.of(3,20)); + + doInJPA(entityManager -> { + Season seasonQ = entityManager + .createQuery( + "select s " + + "from Season s " + + "where " + + "s.beginningOfSeason = :beginningOfSeason", Season.class) + .setParameter("beginningOfSeason", MonthDay.of(12,21)) + .getSingleResult(); + + assertEquals("Winter", seasonQ.getName()); + }); + + assertEquals(COLUMN_TYPE, getColumnType("end_of_season") ); + assertEquals(COLUMN_TYPE, getColumnType("beginning_of_season") ); + } + + public Season createEntity(String name, MonthDay beginning, MonthDay end){ + Season season = new Season(); + season.setName(name); + season.setBeginningOfSeason(beginning); + season.setEndOfSeason(end); + + doInJPA(entityManager -> { + entityManager.persist(season); + }); + + return season; + } + + public String getColumnType(String column){ + ArrayList results = new ArrayList<>(1); + doInJPA(entityManager -> { + Object result = entityManager.createNativeQuery("SELECT data_type FROM information_schema.columns WHERE \n" + + "table_name = 'season' AND column_name = :column_name") + .setParameter("column_name", column) + .getSingleResult(); + results.add((String) result); + }); + return results.get(0); + } + + + @Entity(name = "Season") + @Table(name = "season") + public static class Season { + + @Id + @GeneratedValue + private Long id; + private String name; + + @Type(MonthDayDateType.class) + @Column(name = "beginning_of_season", columnDefinition = "date") + private MonthDay beginningOfSeason; + + @Type(MonthDayDateType.class) + @Column(name = "end_of_season", columnDefinition = "date") + private MonthDay endOfSeason; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public MonthDay getBeginningOfSeason() { + return beginningOfSeason; + } + + public void setBeginningOfSeason(MonthDay beginningOfSeason) { + this.beginningOfSeason = beginningOfSeason; + } + + public MonthDay getEndOfSeason() { + return endOfSeason; + } + + public void setEndOfSeason(MonthDay endOfSeason) { + this.endOfSeason = endOfSeason; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/MySQLMonthDayIntegerTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/MySQLMonthDayIntegerTest.java new file mode 100644 index 000000000..0b0db8b9f --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/MySQLMonthDayIntegerTest.java @@ -0,0 +1,139 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.MonthDay; +import java.util.ArrayList; + +import static org.junit.Assert.assertEquals; + +/** + * @author Mladen Savic (mladensavic94@gmail.com) + */ +public class MySQLMonthDayIntegerTest extends AbstractMySQLIntegrationTest { + + public static final String COLUMN_TYPE = "int"; + + @Override + protected Class[] entities() { + return new Class[]{Season.class}; + } + + @Test + public void testCreationAndFetchById(){ + Season season = createEntity("Summer", MonthDay.of(6,21), MonthDay.of(9,22)); + + doInJPA(entityManager -> { + Season summer = entityManager.unwrap(Session.class).find(Season.class, season.getId()); + + assertEquals(summer.getBeginningOfSeason(), MonthDay.of(6,21)); + assertEquals(summer.getEndOfSeason(), MonthDay.of(9,22)); + }); + + assertEquals(COLUMN_TYPE, getColumnType("end_of_season") ); + assertEquals(COLUMN_TYPE, getColumnType("beginning_of_season") ); + + } + + @Test + public void testFetchWithQuery(){ + createEntity("Winter", MonthDay.of(12,21), MonthDay.of(3,20)); + + doInJPA(entityManager -> { + Season seasonQ = entityManager + .createQuery( + "select s " + + "from Season s " + + "where " + + "s.beginningOfSeason = :beginningOfSeason", Season.class) + .setParameter("beginningOfSeason", MonthDay.of(12,21)) + .getSingleResult(); + + assertEquals("Winter", seasonQ.getName()); + }); + + assertEquals(COLUMN_TYPE, getColumnType("end_of_season") ); + assertEquals(COLUMN_TYPE, getColumnType("beginning_of_season") ); + + } + + public Season createEntity(String name, MonthDay beginning, MonthDay end){ + Season season = new Season(); + season.setName(name); + season.setBeginningOfSeason(beginning); + season.setEndOfSeason(end); + + doInJPA(entityManager -> { + entityManager.persist(season); + }); + + return season; + } + + public String getColumnType(String column){ + ArrayList results = new ArrayList<>(1); + doInJPA(entityManager -> { + Object result = entityManager.createNativeQuery("SELECT data_type FROM information_schema.columns WHERE \n" + + "table_name = 'season' AND column_name = :column_name") + .setParameter("column_name", column) + .getSingleResult(); + results.add((String) result); + }); + return results.get(0); + } + + + @Entity(name = "Season") + @Table(name = "season") + public static class Season { + + @Id + @GeneratedValue + private Long id; + private String name; + + @Type(MonthDayIntegerType.class) + @Column(name = "beginning_of_season", columnDefinition = "integer") + private MonthDay beginningOfSeason; + + @Type(MonthDayIntegerType.class) + @Column(name = "end_of_season", columnDefinition = "integer") + private MonthDay endOfSeason; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public MonthDay getBeginningOfSeason() { + return beginningOfSeason; + } + + public void setBeginningOfSeason(MonthDay beginningOfSeason) { + this.beginningOfSeason = beginningOfSeason; + } + + public MonthDay getEndOfSeason() { + return endOfSeason; + } + + public void setEndOfSeason(MonthDay endOfSeason) { + this.endOfSeason = endOfSeason; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/MySQLYearMonthDateTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/MySQLYearMonthDateTest.java new file mode 100644 index 000000000..ef941a51f --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/MySQLYearMonthDateTest.java @@ -0,0 +1,147 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.YearMonth; +import java.util.TimeZone; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class MySQLYearMonthDateTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + private TimeZone defaultTimeZone; + + @Override + protected void afterInit() { + defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Athens")); + } + + @Override + public void destroy() { + super.destroy(); + TimeZone.setDefault(defaultTimeZone); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(YearMonth.of(2016, 10)); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(YearMonth.of(2016, 10), book.getPublishedOn()); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where " + + " b.title = :title and " + + " b.publishedOn = :publishedOn", Book.class) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("publishedOn", YearMonth.of(2016, 10)) + .getSingleResult(); + + assertEquals("978-9730228236", book.getIsbn()); + }); + } + + @Test + public void testNull() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("123-456"); + book.setPublishedOn(null); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("123-456"); + + assertNull(book.getPublishedOn()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Type(YearMonthDateType.class) + @Column(name = "published_on", columnDefinition = "date") + private YearMonth publishedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public YearMonth getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(YearMonth publishedOn) { + this.publishedOn = publishedOn; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/MySQLYearMonthIntegerTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/MySQLYearMonthIntegerTest.java new file mode 100644 index 000000000..8cc02491a --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/MySQLYearMonthIntegerTest.java @@ -0,0 +1,132 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.YearMonth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class MySQLYearMonthIntegerTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(YearMonth.of(2016, 10)); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(YearMonth.of(2016, 10), book.getPublishedOn()); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where " + + " b.title = :title and " + + " b.publishedOn = :publishedOn", Book.class) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("publishedOn", YearMonth.of(2016, 10)) + .getSingleResult(); + + assertEquals("978-9730228236", book.getIsbn()); + }); + } + + @Test + public void testNull() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("123-456"); + book.setPublishedOn(null); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("123-456"); + + assertNull(book.getPublishedOn()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Type(YearMonthIntegerType.class) + @Column(name = "published_on", columnDefinition = "mediumint") + private YearMonth publishedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public YearMonth getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(YearMonth publishedOn) { + this.publishedOn = publishedOn; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/NullableCharacterTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/NullableCharacterTypeTest.java new file mode 100644 index 000000000..c52255b4c --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/NullableCharacterTypeTest.java @@ -0,0 +1,82 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractTest; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class NullableCharacterTypeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class + }; + } + + @Override + public void init() { + super.init(); + doInJDBC(connection -> { + try ( + Statement statement = connection.createStatement(); + ) { + statement.executeUpdate("INSERT INTO EVENT (ID, EVENT_TYPE) VALUES (1, 'abc')"); + statement.executeUpdate("INSERT INTO EVENT (ID, EVENT_TYPE) VALUES (2, '')"); + statement.executeUpdate("INSERT INTO EVENT (ID, EVENT_TYPE) VALUES (3, 'b')"); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + } + + @Test + public void test() { + final AtomicReference eventHolder = new AtomicReference<>(); + doInJPA(entityManager -> { + List events = entityManager.createQuery("select e from Event e", Event.class).getResultList(); + for (Event event : events) { + LOGGER.info("Event type: {}", event.getType()); + } + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + @GeneratedValue + private Long id; + + @Type(NullableCharacterType.class) + @Column(name = "event_type") + private Character type; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Character getType() { + return type; + } + + public void setType(Character type) { + this.type = type; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/OracleBinaryDoubleTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/OracleBinaryDoubleTest.java new file mode 100644 index 000000000..6d9a000f5 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/OracleBinaryDoubleTest.java @@ -0,0 +1,114 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class OracleBinaryDoubleTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setRating(1.234d); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(1.234d, book.getRating(), 0.001); + }); + } + + @Test + public void testNull() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("123-456"); + book.setRating(null); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("123-456"); + + assertNull(book.getRating()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Column(columnDefinition = "binary_double") + private Double rating; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Double getRating() { + return rating; + } + + public void setRating(Double rating) { + this.rating = rating; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/OracleBinaryFloatTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/OracleBinaryFloatTest.java new file mode 100644 index 000000000..d79b484c9 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/OracleBinaryFloatTest.java @@ -0,0 +1,114 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class OracleBinaryFloatTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setRating(1.234f); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(1.234f, book.getRating(), 0.001); + }); + } + + @Test + public void testNull() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("123-456"); + book.setRating(null); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("123-456"); + + assertNull(book.getRating()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Column(columnDefinition = "binary_float") + private Float rating; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Float getRating() { + return rating; + } + + public void setRating(Float rating) { + this.rating = rating; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLCITextTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLCITextTypeTest.java new file mode 100644 index 000000000..6964f2c80 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLCITextTypeTest.java @@ -0,0 +1,162 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.transaction.JPATransactionFunction; +import jakarta.persistence.*; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Sergei Portnov + */ +public class PostgreSQLCITextTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Country.class + }; + } + + @Override + public void init() { + DataSource dataSource = newDataSource(); + Connection connection = null; + Statement statement = null; + try { + connection = dataSource.getConnection(); + statement = connection.createStatement(); + + statement.executeUpdate("CREATE EXTENSION IF NOT EXISTS citext"); + } catch (SQLException e) { + fail(e.getMessage()); + } finally { + if(statement != null) { + try { + statement.close(); + } catch (SQLException e) { + fail(e.getMessage()); + } + } + if(connection != null) { + try { + connection.close(); + } catch (SQLException e) { + fail(e.getMessage()); + } + } + } + super.init(); + } + + @Test + public void test() { + Country countryWithNullName = new Country(); + countryWithNullName.setId(1L); + persist(countryWithNullName); + testFindById(countryWithNullName.getId(), countryWithNullName.getName()); + + Country countryWithName = new Country(); + countryWithName.setId(2L); + countryWithName.setName("Test"); + persist(countryWithName); + testFindById(countryWithName.getId(), countryWithName.getName()); + + testFindCountryByName(countryWithName.getName(), countryWithName); + testFindCountryByName(countryWithName.getName().toUpperCase(), countryWithName); + testFindCountryByName(countryWithName.getName().toLowerCase(), countryWithName); + } + + private void persist(final Country country) { + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + entityManager.persist(country); + + return null; + } + }); + } + + private void testFindById(final Long countryId, final String expectedName) { + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + Country country = entityManager.find(Country.class, countryId); + + assertEquals(expectedName, country.getName()); + + return null; + } + }); + } + + private void testFindCountryByName(final String searchableName, final Country expectedCountry) { + doInJPA(new JPATransactionFunction() { + @Override + public Void apply(EntityManager entityManager) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery criteria = builder.createQuery(Country.class); + + Root root = criteria.from(Country.class); + + criteria.where( + builder.equal(root.get("name"), searchableName) + ); + + List countries = entityManager + .createQuery(criteria).getResultList(); + + assertEquals(1, countries.size()); + + Country country = countries.iterator().next(); + + assertEquals(expectedCountry.getId(), country.getId()); + assertEquals(expectedCountry.getName(), country.getName()); + + return null; + } + }); + } + + @Table(name = "country") + @Entity(name = "Country") + public static class Country { + + @Id + private Long id; + + @Type(PostgreSQLCITextType.class) + @Column(columnDefinition = "citext") + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLEnumTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLEnumTest.java new file mode 100644 index 000000000..10cf61313 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLEnumTest.java @@ -0,0 +1,131 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.annotations.Type; +import org.hibernate.query.TypedParameterValue; +import org.hibernate.type.CustomType; +import org.junit.Before; +import org.junit.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLEnumTest extends AbstractPostgreSQLIntegrationTest { + + public static final CustomType POST_STATUS_TYPE = new CustomType(new PostgreSQLEnumType(PostStatus.class), null); + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + public void init() { + DataSource dataSource = newDataSource(); + try (Connection connection = dataSource.getConnection()) { + try (Statement statement = connection.createStatement()) { + try { + statement.executeUpdate( + "DROP TYPE post_status_info CASCADE" + ); + } catch (SQLException ignore) { + } + statement.executeUpdate( + "CREATE TYPE post_status_info AS ENUM ('PENDING', 'APPROVED', 'SPAM')" + ); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + super.init(); + } + + @Before + public void setUp() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + post.setStatus(PostStatus.PENDING); + entityManager.persist(post); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(PostStatus.PENDING, post.getStatus()); + }); + } + + @Test + public void testTypedParameterValue() { + doInJPA(entityManager -> { + entityManager.createQuery("SELECT a FROM Post a WHERE a.status = :paramValue", Post.class) + .setParameter("paramValue", new TypedParameterValue(POST_STATUS_TYPE, PostStatus.APPROVED)) + .getResultList(); + }); + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM; + + @Override + public String toString() { + return String.format("The %s enum is mapped to ordinal: %d", name(), ordinal()); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Enumerated(EnumType.STRING) + @Column(columnDefinition = "post_status_info") + //@Ignore("TODO: Unsupported YET!!!") + //@Type(PostgreSQLEnumType.class) + private PostStatus status; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public PostStatus getStatus() { + return status; + } + + public void setStatus(PostStatus status) { + this.status = status; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLHStoreTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLHStoreTypeTest.java new file mode 100644 index 000000000..e18efdb85 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLHStoreTypeTest.java @@ -0,0 +1,101 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import jakarta.persistence.*; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Edgar Asatryan + * @author Vlad Mihalcea + */ +public class PostgreSQLHStoreTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + public void init() { + DataSource dataSource = newDataSource(); + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE EXTENSION IF NOT EXISTS hstore"); + } catch (SQLException e) { + fail(e.getMessage()); + } + super.init(); + } + + @Test + public void test() { + + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.getProperties().put("title", "High-Performance Java Persistence"); + book.getProperties().put("author", "Vlad Mihalcea"); + book.getProperties().put("publisher", "Amazon"); + book.getProperties().put("price", "$44.95"); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals("High-Performance Java Persistence", book.getProperties().get("title")); + assertEquals("Vlad Mihalcea", book.getProperties().get("author")); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Type(PostgreSQLHStoreType.class) + @Column(columnDefinition = "hstore") + private Map properties = new HashMap<>(); + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLInetTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLInetTypeTest.java new file mode 100644 index 000000000..24f1c4465 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLInetTypeTest.java @@ -0,0 +1,145 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLInetTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + private Event _event; + + @Override + public void afterInit() { + doInJDBC(connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE INDEX ON event USING gist (ip inet_ops)"); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + + _event = doInJPA(entityManager -> { + entityManager.persist(new Event()); + + Event event = new Event(); + event.setIp("192.168.0.123/24"); + entityManager.persist(event); + + return event; + }); + } + + @Test + public void testFindById() { + Event updatedEvent = doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + + assertEquals("192.168.0.123/24", event.getIp().getAddress()); + assertEquals("192.168.0.123", event.getIp().toInetAddress().getHostAddress()); + + event.setIp("192.168.0.231/24"); + + return event; + }); + + assertEquals("192.168.0.231/24", updatedEvent.getIp().getAddress()); + } + + @Test + public void testJPQLQuery() { + doInJPA(entityManager -> { + Event event = entityManager.createQuery( + "select e " + + "from Event e " + + "where " + + " ip is not null", Event.class) + .getSingleResult(); + + assertEquals("192.168.0.123/24", event.getIp().getAddress()); + }); + } + + @Test + public void testNativeQuery() { + doInJPA(entityManager -> { + Event event = (Event) entityManager.createNativeQuery( + "SELECT e.* " + + "FROM event e " + + "WHERE " + + " e.ip && CAST(:network AS inet) = true", Event.class) + .setParameter("network", "192.168.0.1/24") + .getSingleResult(); + + assertEquals("192.168.0.123/24", event.getIp().getAddress()); + }); + } + + + @Test + public void testJDBCQuery() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + session.doWork(connection -> { + try(PreparedStatement ps = connection.prepareStatement( + "SELECT * " + + "FROM Event e " + + "WHERE " + + " e.ip && ?::inet = true" + )) { + ps.setObject(1, "192.168.0.1/24"); + ResultSet rs = ps.executeQuery(); + while(rs.next()) { + Long id = rs.getLong(1); + String ip = rs.getString(2); + assertEquals("192.168.0.123/24", ip); + } + } + }); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + @GeneratedValue + private Long id; + + @Type(PostgreSQLInetType.class) + @Column(name = "ip", columnDefinition = "inet") + private Inet ip; + + public Long getId() { + return id; + } + + public Inet getIp() { + return ip; + } + + public void setIp(String address) { + this.ip = new Inet(address); + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLMonthDayDateTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLMonthDayDateTest.java new file mode 100644 index 000000000..2aa9875ef --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLMonthDayDateTest.java @@ -0,0 +1,138 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.MonthDay; +import java.util.ArrayList; + +import static org.junit.Assert.assertEquals; + +/** + * @author Mladen Savic (mladensavic94@gmail.com) + */ +public class PostgreSQLMonthDayDateTest extends AbstractPostgreSQLIntegrationTest { + + public static final String COLUMN_TYPE = "date"; + + @Override + protected Class[] entities() { + return new Class[]{Season.class}; + } + + @Test + public void testCreationAndFetchById(){ + Season season = createEntity("Summer", MonthDay.of(6,21), MonthDay.of(9,22)); + + doInJPA(entityManager -> { + Season summer = entityManager.unwrap(Session.class).find(Season.class, season.getId()); + + assertEquals(summer.getBeginningOfSeason(), MonthDay.of(6,21)); + assertEquals(summer.getEndOfSeason(), MonthDay.of(9,22)); + }); + + assertEquals(COLUMN_TYPE, getColumnType("end_of_season") ); + assertEquals(COLUMN_TYPE, getColumnType("beginning_of_season") ); + + } + + @Test + public void testFetchWithQuery(){ + createEntity("Winter", MonthDay.of(12,21), MonthDay.of(3,20)); + + doInJPA(entityManager -> { + Season seasonQ = entityManager + .createQuery( + "select s " + + "from Season s " + + "where " + + "s.beginningOfSeason = :beginningOfSeason", Season.class) + .setParameter("beginningOfSeason", MonthDay.of(12,21)) + .getSingleResult(); + + assertEquals("Winter", seasonQ.getName()); + }); + + assertEquals(COLUMN_TYPE, getColumnType("end_of_season") ); + assertEquals(COLUMN_TYPE, getColumnType("beginning_of_season") ); + } + + public Season createEntity(String name, MonthDay beginning, MonthDay end){ + Season season = new Season(); + season.setName(name); + season.setBeginningOfSeason(beginning); + season.setEndOfSeason(end); + + doInJPA(entityManager -> { + entityManager.persist(season); + }); + + return season; + } + + public String getColumnType(String column){ + ArrayList results = new ArrayList<>(1); + doInJPA(entityManager -> { + Object result = entityManager.createNativeQuery("SELECT data_type FROM information_schema.columns WHERE \n" + + "table_name = 'season' AND column_name = :column_name") + .setParameter("column_name", column) + .getSingleResult(); + results.add((String) result); + }); + return results.get(0); + } + + + @Entity(name = "Season") + @Table(name = "season") + public static class Season { + + @Id + @GeneratedValue + private Long id; + private String name; + + @Type(MonthDayDateType.class) + @Column(name = "beginning_of_season", columnDefinition = "date") + private MonthDay beginningOfSeason; + + @Type(MonthDayDateType.class) + @Column(name = "end_of_season", columnDefinition = "date") + private MonthDay endOfSeason; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public MonthDay getBeginningOfSeason() { + return beginningOfSeason; + } + + public void setBeginningOfSeason(MonthDay beginningOfSeason) { + this.beginningOfSeason = beginningOfSeason; + } + + public MonthDay getEndOfSeason() { + return endOfSeason; + } + + public void setEndOfSeason(MonthDay endOfSeason) { + this.endOfSeason = endOfSeason; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLMonthDayIntegerTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLMonthDayIntegerTest.java new file mode 100644 index 000000000..f63f6c79f --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLMonthDayIntegerTest.java @@ -0,0 +1,135 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.MonthDay; +import java.util.ArrayList; + +import static org.junit.Assert.assertEquals; + +/** + * @author Mladen Savic (mladensavic94@gmail.com) + */ +public class PostgreSQLMonthDayIntegerTest extends AbstractPostgreSQLIntegrationTest { + + public static final String COLUMN_TYPE = "integer"; + @Override + protected Class[] entities() { + return new Class[]{Season.class}; + } + + @Test + public void testCreationAndFetchById(){ + Season season = createEntity("Summer", MonthDay.of(6,21), MonthDay.of(9,22)); + + doInJPA(entityManager -> { + Season summer = entityManager.unwrap(Session.class).find(Season.class, season.getId()); + + assertEquals(summer.getBeginningOfSeason(), MonthDay.of(6,21)); + assertEquals(summer.getEndOfSeason(), MonthDay.of(9,22)); + }); + + assertEquals(COLUMN_TYPE, getColumnType("end_of_season") ); + assertEquals(COLUMN_TYPE, getColumnType("beginning_of_season") ); + } + + @Test + public void testFetchWithQuery(){ + createEntity("Winter", MonthDay.of(12,21), MonthDay.of(3,20)); + + doInJPA(entityManager -> { + Season seasonQ = entityManager + .createQuery( + "select s " + + "from Season s " + + "where " + + "s.beginningOfSeason = :beginningOfSeason", Season.class) + .setParameter("beginningOfSeason", MonthDay.of(12,21)) + .getSingleResult(); + + assertEquals("Winter", seasonQ.getName()); + }); + assertEquals(COLUMN_TYPE, getColumnType("end_of_season") ); + assertEquals(COLUMN_TYPE, getColumnType("beginning_of_season") ); + } + + public Season createEntity(String name, MonthDay beginning, MonthDay end){ + Season season = new Season(); + season.setName(name); + season.setBeginningOfSeason(beginning); + season.setEndOfSeason(end); + + doInJPA(entityManager -> { + entityManager.persist(season); + }); + + return season; + } + + public String getColumnType(String column){ + ArrayList results = new ArrayList<>(1); + doInJPA(entityManager -> { + Object result = entityManager.createNativeQuery("SELECT data_type FROM information_schema.columns WHERE \n" + + "table_name = 'season' AND column_name = :column_name") + .setParameter("column_name", column) + .getSingleResult(); + results.add((String) result); + }); + return results.get(0); + } + + + @Entity(name = "Season") + @Table(name = "season") + public static class Season { + + @Id + @GeneratedValue + private Long id; + private String name; + + @Type(MonthDayIntegerType.class) + @Column(name = "beginning_of_season", columnDefinition = "integer") + private MonthDay beginningOfSeason; + + @Type(MonthDayIntegerType.class) + @Column(name = "end_of_season", columnDefinition = "integer") + private MonthDay endOfSeason; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public MonthDay getBeginningOfSeason() { + return beginningOfSeason; + } + + public void setBeginningOfSeason(MonthDay beginningOfSeason) { + this.beginningOfSeason = beginningOfSeason; + } + + public MonthDay getEndOfSeason() { + return endOfSeason; + } + + public void setEndOfSeason(MonthDay endOfSeason) { + this.endOfSeason = endOfSeason; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthDateTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthDateTest.java new file mode 100644 index 000000000..b03994249 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthDateTest.java @@ -0,0 +1,132 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.YearMonth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLYearMonthDateTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(YearMonth.of(2016, 10)); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(YearMonth.of(2016, 10), book.getPublishedOn()); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where " + + " b.title = :title and " + + " b.publishedOn = :publishedOn", Book.class) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("publishedOn", YearMonth.of(2016, 10)) + .getSingleResult(); + + assertEquals("978-9730228236", book.getIsbn()); + }); + } + + @Test + public void testNull() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("123-456"); + book.setPublishedOn(null); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("123-456"); + + assertNull(book.getPublishedOn()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Type(YearMonthDateType.class) + @Column(name = "published_on", columnDefinition = "date") + private YearMonth publishedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public YearMonth getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(YearMonth publishedOn) { + this.publishedOn = publishedOn; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthEpochTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthEpochTest.java new file mode 100644 index 000000000..47ce89412 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthEpochTest.java @@ -0,0 +1,189 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.query.NativeQuery; +import org.junit.Ignore; +import org.junit.Test; + +import java.time.YearMonth; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLYearMonthEpochTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, 50); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(YearMonth.of(2016, 10)); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(YearMonth.of(2016, 10), book.getPublishedOn()); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where " + + " b.title = :title and " + + " b.publishedOn = :publishedOn", Book.class) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("publishedOn", YearMonth.of(2016, 10)) + .getSingleResult(); + + assertEquals("978-9730228236", book.getIsbn()); + }); + } + + @Test + public void testNull() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("123-456"); + book.setPublishedOn(null); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("123-456"); + + assertNull(book.getPublishedOn()); + }); + } + + @Test + @Ignore + public void testIndexing() { + doInJPA(entityManager -> { + + YearMonth yearMonth = YearMonth.of(1970, 1); + + for (int i = 0; i < 5000; i++) { + yearMonth = yearMonth.plusMonths(1); + + Book book = new Book(); + book.setTitle( + String.format( + "IT industry newsletter - %s edition", yearMonth + ) + ); + book.setPublishedOn(yearMonth); + + entityManager.persist(book); + } + }); + + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.createNativeQuery( + "EXPLAIN ANALYZE " + + "SELECT " + + " b.published_on " + + "FROM " + + " book b " + + "WHERE " + + " b.published_on BETWEEN :startYearMonth AND :endYearMonth ") + .unwrap(NativeQuery.class) + .setParameter("startYearMonth", YearMonth.of(2010, 12), YearMonthEpochType.INSTANCE) + .setParameter("endYearMonth", YearMonth.of(2018, 1), YearMonthEpochType.INSTANCE) + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", executionPlanLines.stream().collect(Collectors.joining("\n"))); + } + + @Entity(name = "Book") + @Table( + name = "book", + indexes = @Index( + name = "idx_book_published_on", + columnList = "published_on" + ) + ) + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Type(YearMonthEpochType.class) + @Column(name = "published_on", columnDefinition = "integer") + private YearMonth publishedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public YearMonth getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(YearMonth publishedOn) { + this.publishedOn = publishedOn; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthIntegerTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthIntegerTest.java new file mode 100644 index 000000000..a99daf35c --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthIntegerTest.java @@ -0,0 +1,189 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.query.NativeQuery; +import org.junit.Ignore; +import org.junit.Test; + +import java.time.YearMonth; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLYearMonthIntegerTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, 50); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(YearMonth.of(2016, 10)); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(YearMonth.of(2016, 10), book.getPublishedOn()); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where " + + " b.title = :title and " + + " b.publishedOn = :publishedOn", Book.class) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("publishedOn", YearMonth.of(2016, 10)) + .getSingleResult(); + + assertEquals("978-9730228236", book.getIsbn()); + }); + } + + @Test + public void testNull() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("123-456"); + book.setPublishedOn(null); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("123-456"); + + assertNull(book.getPublishedOn()); + }); + } + + @Test + @Ignore + public void testIndexing() { + doInJPA(entityManager -> { + + YearMonth yearMonth = YearMonth.of(1970, 1); + + for (int i = 0; i < 5000; i++) { + yearMonth = yearMonth.plusMonths(1); + + Book book = new Book(); + book.setTitle( + String.format( + "IT industry newsletter - %s edition", yearMonth + ) + ); + book.setPublishedOn(yearMonth); + + entityManager.persist(book); + } + }); + + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.createNativeQuery( + "EXPLAIN ANALYZE " + + "SELECT " + + " b.published_on " + + "FROM " + + " book b " + + "WHERE " + + " b.published_on BETWEEN :startYearMonth AND :endYearMonth ") + .unwrap(NativeQuery.class) + .setParameter("startYearMonth", YearMonth.of(2010, 12), YearMonthIntegerType.INSTANCE) + .setParameter("endYearMonth", YearMonth.of(2018, 1), YearMonthIntegerType.INSTANCE) + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", executionPlanLines.stream().collect(Collectors.joining("\n"))); + } + + @Entity(name = "Book") + @Table( + name = "book", + indexes = @Index( + name = "idx_book_published_on", + columnList = "published_on" + ) + ) + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Type(YearMonthIntegerType.class) + @Column(name = "published_on", columnDefinition = "integer") + private YearMonth publishedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public YearMonth getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(YearMonth publishedOn) { + this.publishedOn = publishedOn; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthIntegerTypeContributorTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthIntegerTypeContributorTest.java new file mode 100644 index 000000000..53dbe7214 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthIntegerTypeContributorTest.java @@ -0,0 +1,122 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.YearMonth; +import java.util.Collections; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLYearMonthIntegerTypeContributorTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.type_contributors", + (TypeContributorList) () -> Collections.singletonList( + (typeContributions, serviceRegistry) -> + typeContributions.contributeType(YearMonthIntegerType.INSTANCE) + )); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(YearMonth.of(2016, 10)); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(YearMonth.of(2016, 10), book.getPublishedOn()); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where " + + " b.title = :title and " + + " b.publishedOn = :publishedOn", Book.class) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("publishedOn", YearMonth.of(2016, 10)) + .getSingleResult(); + + assertEquals("978-9730228236", book.getIsbn()); + }); + } + + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Column(name = "published_on", columnDefinition = "integer") + private YearMonth publishedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public YearMonth getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(YearMonth publishedOn) { + this.publishedOn = publishedOn; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthTimestampTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthTimestampTest.java new file mode 100644 index 000000000..2ced915ab --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/PostgreSQLYearMonthTimestampTest.java @@ -0,0 +1,132 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.YearMonth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLYearMonthTimestampTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(YearMonth.of(2016, 10)); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(YearMonth.of(2016, 10), book.getPublishedOn()); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .createQuery( + "select b " + + "from Book b " + + "where " + + " b.title = :title and " + + " b.publishedOn = :publishedOn", Book.class) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("publishedOn", YearMonth.of(2016, 10)) + .getSingleResult(); + + assertEquals("978-9730228236", book.getIsbn()); + }); + } + + @Test + public void testNull() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("123-456"); + book.setPublishedOn(null); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("123-456"); + + assertNull(book.getPublishedOn()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Type(YearMonthTimestampType.class) + @Column(name = "published_on", columnDefinition = "date") + private YearMonth publishedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public YearMonth getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(YearMonth publishedOn) { + this.publishedOn = publishedOn; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/YearAndMonthTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/YearAndMonthTest.java new file mode 100644 index 000000000..84d42a287 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/YearAndMonthTest.java @@ -0,0 +1,115 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.Month; +import java.time.Year; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class YearAndMonthTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Publisher.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Publisher publisher = new Publisher(); + publisher.setName("vladmihalcea.com"); + publisher.setEstYear(Year.of(2013)); + publisher.setSalesMonth(Month.NOVEMBER); + + entityManager.persist(publisher); + }); + + doInJPA(entityManager -> { + Publisher publisher = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Publisher.class) + .load("vladmihalcea.com"); + + assertEquals(Year.of(2013), publisher.getEstYear()); + assertEquals(Month.NOVEMBER, publisher.getSalesMonth()); + }); + + doInJPA(entityManager -> { + Publisher book = entityManager + .createQuery( + "select p " + + "from Publisher p " + + "where " + + " p.estYear = :estYear and " + + " p.salesMonth = :salesMonth", Publisher.class) + .setParameter("estYear", Year.of(2013)) + .setParameter("salesMonth", Month.NOVEMBER) + .getSingleResult(); + + assertEquals("vladmihalcea.com", book.getName()); + }); + } + + @Entity(name = "Publisher") + @Table(name = "publisher") + public static class Publisher { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @Type(YearType.class) + @Column(name = "est_year", columnDefinition = "smallint") + private Year estYear; + + @Column(name = "sales_month", columnDefinition = "smallint") + @Enumerated + private Month salesMonth; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Year getEstYear() { + return estYear; + } + + public void setEstYear(Year estYear) { + this.estYear = estYear; + } + + public Month getSalesMonth() { + return salesMonth; + } + + public void setSalesMonth(Month salesMonth) { + this.salesMonth = salesMonth; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/ZoneIdTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/ZoneIdTest.java new file mode 100644 index 000000000..52b793e7b --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/basic/ZoneIdTest.java @@ -0,0 +1,98 @@ +package com.vladmihalcea.hibernate.type.basic; + +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.ZoneId; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for {@see ZoneId} Hibernate mapping. + * + * @author stonio + */ +public class ZoneIdTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{UserPreferences.class}; + } + + @Test + public void test() { + doInJPA(entityManager -> { + UserPreferences UserPreferences = new UserPreferences(); + UserPreferences.setName("vladmihalcea.com"); + UserPreferences.setZoneId(ZoneId.of("Europe/Bucharest")); + + entityManager.persist(UserPreferences); + }); + + doInJPA(entityManager -> { + UserPreferences userPreferences = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(UserPreferences.class) + .load("vladmihalcea.com"); + + assertEquals(ZoneId.of("Europe/Bucharest"), userPreferences.getZoneId()); + }); + + doInJPA(entityManager -> { + UserPreferences prefs = entityManager + .createQuery( + "select p " + + "from UserPreferences p " + + "where " + + " p.zoneId = :zoneId", UserPreferences.class) + .setParameter("zoneId", ZoneId.of("Europe/Bucharest")) + .getSingleResult(); + + assertEquals("vladmihalcea.com", prefs.getName()); + }); + } + + @Entity(name = "UserPreferences") + @Table(name = "user_preferences") + public static class UserPreferences { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @Type(ZoneIdType.class) + @Column(name = "zone_id", length= 40) + private ZoneId zoneId; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ZoneId getZoneId() { + return zoneId; + } + + public void setZoneId(ZoneId zoneId) { + this.zoneId = zoneId; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/binary/MySQLBinaryTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/binary/MySQLBinaryTypeTest.java new file mode 100644 index 000000000..c2453e75b --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/binary/MySQLBinaryTypeTest.java @@ -0,0 +1,80 @@ +package com.vladmihalcea.hibernate.type.binary; + +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLBinaryTypeTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + .setImage(new byte[]{1, 2, 3}) + ); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertArrayEquals( + new byte[]{1, 2, 3}, + post.getImage() + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Column(columnDefinition = "BINARY(3)") + private byte[] image; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public byte[] getImage() { + return image; + } + + public Post setImage(byte[] image) { + this.image = image; + return this; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/interval/OracleIntervalDayToSecondTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/interval/OracleIntervalDayToSecondTypeTest.java new file mode 100644 index 000000000..54442022c --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/interval/OracleIntervalDayToSecondTypeTest.java @@ -0,0 +1,59 @@ +package com.vladmihalcea.hibernate.type.interval; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.Duration; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for {@see OracleIntervalDayToSecondType} Hibernate type. + * + * @author Vlad Mihalcea + */ +public class OracleIntervalDayToSecondTypeTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{WorkShift.class}; + } + + @Test + public void test() { + Duration duration = Duration.ofDays(1).plusHours(2).plusMinutes(3).plusSeconds(4); + + doInJPA(entityManager -> { + WorkShift intervalEntity = new WorkShift(); + intervalEntity.setId(1L); + intervalEntity.setDuration(duration); + + entityManager.persist(intervalEntity); + }); + + doInJPA(entityManager -> { + WorkShift result = entityManager.find(WorkShift.class, 1L); + assertEquals(duration, result.getDuration()); + }); + } + + @Entity(name = "WorkShift") + public static class WorkShift extends BaseEntity { + + @Type(OracleIntervalDayToSecondType.class) + @Column(columnDefinition = "INTERVAL DAY TO SECOND") + private Duration duration; + + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/interval/PostgreSQLIntervalTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/interval/PostgreSQLIntervalTypeTest.java new file mode 100644 index 000000000..1b04c8ff7 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/interval/PostgreSQLIntervalTypeTest.java @@ -0,0 +1,59 @@ +package com.vladmihalcea.hibernate.type.interval; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.Duration; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for {@see PostgreSQLIntervalType} Hibernate type. + * + * @author Jan-Willem Gmelig Meyling + */ +public class PostgreSQLIntervalTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{WorkShift.class}; + } + + @Test + public void test() { + Duration duration = Duration.ofDays(1).plusHours(2).plusMinutes(3).plusSeconds(4).plusNanos(5000); + + doInJPA(entityManager -> { + WorkShift intervalEntity = new WorkShift(); + intervalEntity.setId(1L); + intervalEntity.setDuration(duration); + + entityManager.persist(intervalEntity); + }); + + doInJPA(entityManager -> { + WorkShift result = entityManager.find(WorkShift.class, 1L); + assertEquals(duration, result.getDuration()); + }); + } + + @Entity(name = "WorkShift") + public static class WorkShift extends BaseEntity { + + @Type(PostgreSQLIntervalType.class) + @Column(columnDefinition = "interval") + private Duration duration; + + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/interval/PostgreSQLPeriodTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/interval/PostgreSQLPeriodTypeTest.java new file mode 100644 index 000000000..6f091e72c --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/interval/PostgreSQLPeriodTypeTest.java @@ -0,0 +1,59 @@ +package com.vladmihalcea.hibernate.type.interval; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.Period; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for {@see PostgreSQLIntervalType} Hibernate type. + * + * @author Jan-Willem Gmelig Meyling + */ +public class PostgreSQLPeriodTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{WorkShift.class}; + } + + @Test + public void test() { + Period duration = Period.of(1, 2, 3); + + doInJPA(entityManager -> { + WorkShift intervalEntity = new WorkShift(); + intervalEntity.setId(1L); + intervalEntity.setDuration(duration); + + entityManager.persist(intervalEntity); + }); + + doInJPA(entityManager -> { + WorkShift result = entityManager.find(WorkShift.class, 1L); + assertEquals(duration, result.getDuration()); + }); + } + + @Entity(name = "WorkShift") + public static class WorkShift extends BaseEntity { + + @Type(PostgreSQLPeriodType.class) + @Column(columnDefinition = "interval") + private Period duration; + + public Period getDuration() { + return duration; + } + + public void setDuration(Period duration) { + this.duration = duration; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/EhcacheMySQLJsonBinaryTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/EhcacheMySQLJsonBinaryTypeTest.java new file mode 100644 index 000000000..4d15c7927 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/EhcacheMySQLJsonBinaryTypeTest.java @@ -0,0 +1,127 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.json.internal.JacksonUtil; +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.type.model.Location; +import com.vladmihalcea.hibernate.type.model.Ticket; +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.Cacheable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class EhcacheMySQLJsonBinaryTypeTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + protected String[] packages() { + return new String[]{ + Event.class.getPackage().getName() + }; + } + + protected void additionalProperties(Properties properties) { + properties.setProperty("hibernate.cache.use_second_level_cache", "true"); + properties.setProperty("hibernate.cache.use_query_cache", "true"); + properties.setProperty("hibernate.cache.region.factory_class", "ehcache"); + } + + private Event _event; + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + + Event event = new Event(); + event.setId(1L); + + event.setProperties( + JacksonUtil.toJsonNode( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + entityManager.persist(event); + + Ticket ticket = new Ticket(); + ticket.setPrice(12.34d); + ticket.setRegistrationCode("ABC123"); + + _event = event; + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + QueryCountHolder.clear(); + + Event event = entityManager.find(Event.class, _event.getId()); + assertNotNull(event.getProperties()); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(0, queryCount.getTotal()); + + List properties = entityManager.createNativeQuery( + "select CAST(e.properties AS CHAR(1000)) " + + "from event e " + + "where JSON_EXTRACT(e.properties, \"$.price\") > 1 ") + .getResultList(); + + assertEquals(1, properties.size()); + JsonNode jsonNode = JacksonUtil.toJsonNode(properties.get(0)); + assertEquals("High-Performance Java Persistence", jsonNode.get("title").asText()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + @Cacheable(true) + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Event extends BaseEntity { + + @Type(JsonNodeStringType.class) + @Column(columnDefinition = "json") + private JsonNode properties; + + public JsonNode getProperties() { + return properties; + } + + public void setProperties(JsonNode properties) { + this.properties = properties; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLGenericJsonTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLGenericJsonTypeTest.java new file mode 100644 index 000000000..ae6fc16ae --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLGenericJsonTypeTest.java @@ -0,0 +1,132 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.type.model.Location; +import com.vladmihalcea.hibernate.type.model.Ticket; +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLGenericJsonTypeTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + Participant.class + }; + } + + @Override + protected String[] packages() { + return new String[]{ + Location.class.getPackage().getName() + }; + } + + private Event _event; + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + + Event event = new Event(); + event.setId(1L); + event.setAlternativeLocations(Arrays.asList(location)); + entityManager.persist(event); + + _event = event; + }); + } + + @Test + public void test() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + assertEquals(1, event.getAlternativeLocations().size()); + assertEquals("Cluj-Napoca", event.getAlternativeLocations().get(0).getCity()); + assertEquals("Romania", event.getAlternativeLocations().get(0).getCountry()); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event extends BaseEntity { + + @Type(JsonType.class) + @Column(columnDefinition = "json") + private Location location; + + @Type(JsonStringType.class) + @Column(columnDefinition = "json") + private List alternativeLocations; + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + + public List getAlternativeLocations() { + return alternativeLocations; + } + + public void setAlternativeLocations(List alternativeLocations) { + this.alternativeLocations = alternativeLocations; + } + } + + @Entity(name = "Participant") + @Table(name = "participant") + public static class Participant extends BaseEntity { + + @Type(JsonType.class) + @Column(columnDefinition = "json") + private Ticket ticket; + + @ManyToOne + private Event event; + + public Ticket getTicket() { + return ticket; + } + + public void setTicket(Ticket ticket) { + this.ticket = ticket; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonNodePropertyTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonNodePropertyTest.java new file mode 100644 index 000000000..b9b8f8386 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonNodePropertyTest.java @@ -0,0 +1,107 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Victor Noël + */ +public class MySQLJsonNodePropertyTest extends AbstractMySQLIntegrationTest { + + private final ObjectMapper mapper = newMapper(); + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + try { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties(mapper.readTree("{\"field\": 0.05}")) + ); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); + } + }); + } + + @Test + public void test() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + assertEquals(0.05, book.getProperties().get("field").asDouble(), 0.0); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(0, queryCount.getUpdate()); + } + + public static class MyJsonType extends JsonType { + public MyJsonType() { + super(newMapper()); + } + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(MyJsonType.class) + @Column(columnDefinition = "json") + private JsonNode properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public JsonNode getProperties() { + return properties; + } + + public Book setProperties(JsonNode properties) { + this.properties = properties; + return this; + } + } + + private static ObjectMapper newMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true); + return mapper; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonStringPropertyTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonStringPropertyTest.java new file mode 100644 index 000000000..ea8e0b11c --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonStringPropertyTest.java @@ -0,0 +1,143 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class MySQLJsonStringPropertyTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + Book book = session + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertTrue(book.getProperties().contains("\"price\": 44.99")); + }); + + queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Test + public void testNullValue() { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties(null); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonStringType.class) + @Column(columnDefinition = "json") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonTypeSetTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonTypeSetTest.java new file mode 100644 index 000000000..b927a68d9 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonTypeSetTest.java @@ -0,0 +1,197 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.beans.ConstructorProperties; +import java.io.Serializable; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; + +/** + * @author Sergei Poznanski + */ +public class MySQLJsonTypeSetTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + User.class + }; + } + + private User _user; + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + User user = new User(); + + user.setId(1L); + user.setPhones(new HashSet<>(asList("7654321", "1234567"))); + user.setRoles(EnumSet.of(Role.ADMIN, Role.USER)); + user.setChildren(new HashSet<>(asList( + new Child("Jane", 2, new HashSet<>(asList("toy1", "toy2"))), + new Child("John", 1, new HashSet<>(asList("toy3", "toy4"))) + ))); + + entityManager.persist(user); + _user = user; + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + User user = entityManager.find(User.class, _user.getId()); + + QueryCountHolder.clear(); + + user.setPhones(new HashSet<>(asList("1592637", "9518473"))); + user.setRoles(EnumSet.of(Role.USER, Role.DEV)); + user.setChildren(new HashSet<>(asList( + new Child("Jinny", 1, new HashSet<>(asList("toy5", "toy6"))), + new Child("Jenny", 2, new HashSet<>(asList("toy7", "toy8"))) + ))); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + + doInJPA(entityManager -> { + User user = entityManager.find(User.class, _user.getId()); + assertEquals(new HashSet<>(asList("9518473", "1592637")), user.getPhones()); + assertEquals(EnumSet.of(Role.DEV, Role.USER), user.getRoles()); + assertEquals(new HashSet<>(asList( + new Child("Jenny", 2, new HashSet<>(asList("toy8", "toy7"))), + new Child("Jinny", 1, new HashSet<>(asList("toy6", "toy5"))) + )), user.getChildren()); + assertEquals(Integer.valueOf(1), user.getVersion()); + }); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + User user = entityManager.find(User.class, _user.getId()); + assertEquals(new HashSet<>(asList("1234567", "7654321")), user.getPhones()); + assertEquals(EnumSet.of(Role.USER, Role.ADMIN), user.getRoles()); + assertEquals(new HashSet<>(asList( + new Child("John", 1, new HashSet<>(asList("toy4", "toy3"))), + new Child("Jane", 2, new HashSet<>(asList("toy2", "toy1"))) + )), user.getChildren()); + assertEquals(Integer.valueOf(0), user.getVersion()); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Entity + @Table(name = "users") + @DynamicUpdate + public static class User extends BaseEntity { + + @Type(JsonType.class) + @Column(nullable = false, columnDefinition = "json") + private Set phones; + + @Type(JsonType.class) + @Column(nullable = false, columnDefinition = "json") + private EnumSet roles; + + @Type(JsonType.class) + @Column(nullable = false, columnDefinition = "json") + private Set children; + + public Set getPhones() { + return phones; + } + + public void setPhones(Set phones) { + this.phones = phones; + } + + public EnumSet getRoles() { + return roles; + } + + public void setRoles(EnumSet roles) { + this.roles = roles; + } + + public Set getChildren() { + return children; + } + + public void setChildren(final Set children) { + this.children = children; + } + } + + public enum Role { + USER, ADMIN, DEV + } + + public static class Child implements Serializable { + + private final String name; + private final Integer age; + private final Set toys; + + @ConstructorProperties({"name", "age", "toys"}) + public Child(String name, Integer age, final Set toys) { + this.name = Objects.requireNonNull(name); + this.age = Objects.requireNonNull(age); + this.toys = Objects.requireNonNull(toys); + } + + public String getName() { + return name; + } + + public Integer getAge() { + return age; + } + + public Set getToys() { + return toys; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Child child = (Child) o; + if (!name.equals(child.name)) return false; + if (!age.equals(child.age)) return false; + return toys.equals(child.toys); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + age.hashCode(); + result = 31 * result + toys.hashCode(); + return result; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonTypeTest.java new file mode 100644 index 000000000..dfa91669a --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLJsonTypeTest.java @@ -0,0 +1,160 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.type.model.Location; +import com.vladmihalcea.hibernate.type.model.Ticket; +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLJsonTypeTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + Participant.class + }; + } + + @Override + protected String[] packages() { + return new String[]{ + Location.class.getPackage().getName() + }; + } + + private Event _event; + + private Participant _participant; + + @Override + protected void afterInit() { + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + + Event event = new Event(); + event.setId(1L); + event.setLocation(location); + entityManager.persist(event); + + Ticket ticket = new Ticket(); + ticket.setPrice(12.34d); + ticket.setRegistrationCode("ABC123"); + + Participant participant = new Participant(); + participant.setId(1L); + participant.setTicket(ticket); + participant.setEvent(event); + + entityManager.persist(participant); + + _event = event; + _participant = participant; + }); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + assertEquals("Romania", event.getLocation().getCountry()); + assertEquals("Cluj-Napoca", event.getLocation().getCity()); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Test + public void test() { + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + assertEquals("Cluj-Napoca", event.getLocation().getCity()); + + Participant participant = entityManager.find(Participant.class, _participant.getId()); + assertEquals("ABC123", participant.getTicket().getRegistrationCode()); + + List participants = entityManager.createNativeQuery( + "select p.ticket -> \"$.registrationCode\" " + + "from participant p " + + "where JSON_EXTRACT(p.ticket, \"$.price\") > 1 ") + .getResultList(); + + event.getLocation().setCity("Constanța"); + entityManager.flush(); + + assertEquals(1, participants.size()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event extends BaseEntity { + + @Type(JsonType.class) + @Column(columnDefinition = "json") + private Location location; + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + } + + @Entity(name = "Participant") + @Table(name = "participant") + public static class Participant extends BaseEntity { + + @Type(JsonType.class) + @Column(columnDefinition = "json") + private Ticket ticket; + + @ManyToOne + private Event event; + + public Ticket getTicket() { + return ticket; + } + + public void setTicket(Ticket ticket) { + this.ticket = ticket; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLParametrizedJsonTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLParametrizedJsonTypeTest.java new file mode 100644 index 000000000..9eac76a76 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/MySQLParametrizedJsonTypeTest.java @@ -0,0 +1,252 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.Serializable; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLParametrizedJsonTypeTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + PropertyHolder.class, + }; + } + + @Override + protected String[] packages() { + return new String[]{ + PropertyHolder.class.getPackage().getName() + }; + } + + /*@Override + protected List additionalTypes() { + + ParametrizedJsonStringType jsonType = new ParametrizedJsonStringType("json-hashmap", HashMap.class, new Class[]{String.class, Object.class}); + return Collections.singletonList(jsonType); + }*/ + + @Override + protected boolean nativeHibernateSessionFactoryBootstrap() { + return true; + } + + @Test + @Ignore("TODO: Unsupported YET!!!") + public void test() { + final AtomicReference eventHolder = new AtomicReference<>(); + + doInJPA(entityManager -> { + PropertyHolder propertyHolder = new PropertyHolder(); + propertyHolder.setId(1L); + propertyHolder.addProperty("key1", "value"); + propertyHolder.addProperty("key2", 123456789); + propertyHolder.addProperty("key3", new POJO("one", "two")); + entityManager.persist(propertyHolder); + + eventHolder.set(propertyHolder); + }); + + doInJPA(entityManager -> { + PropertyHolder propertyHolder = entityManager.find(PropertyHolder.class, eventHolder.get().getId()); + assertEquals("value", propertyHolder.getProperty("key1")); + assertEquals(123456789, propertyHolder.getProperty("key2")); + assertEquals(new POJO("one", "two"), propertyHolder.getProperty("key3")); + + }); + } + + public static class ParametrizedJsonStringType extends JsonStringType { + + private String name; + + public ParametrizedJsonStringType(String name, final Type rawType, final Type[] actualTypeArguments) { + super(createObjectMapper(), createParameterizedType(rawType, actualTypeArguments, null)); + this.name = name; + } + + @Override + public String getName() { + return name; + } + + private static ObjectMapper createObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.enableDefaultTyping(); + return objectMapper; + } + + private static ParameterizedType createParameterizedType(final Type rawType, final Type[] actualTypeArguments, final Type ownerType) { + return new ParameterizedType() { + + @Override + public Type[] getActualTypeArguments() { + return actualTypeArguments; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public Type getOwnerType() { + return ownerType; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ParameterizedType)) { + return false; + } + ParameterizedType other = (ParameterizedType) obj; + return Arrays.equals(getActualTypeArguments(), other.getActualTypeArguments()) && safeEquals(getRawType(), other.getRawType()) && safeEquals(getOwnerType(), other.getOwnerType()); + } + + @Override + public int hashCode() { + return safeHashCode(getActualTypeArguments()) ^ safeHashCode(getRawType()) ^ safeHashCode(getOwnerType()); + } + }; + } + + private static boolean safeEquals(Type t1, Type t2) { + if (t1 == null) { + return t2 == null; + } + return t1.equals(t2); + } + + private static int safeHashCode(Object o) { + if (o == null) { + return 1; + } + return o.hashCode(); + } + + } + + @Entity(name = "PropertyHolder") + @Table(name = "propertyholder") + public static class PropertyHolder { + + @Id + private Long id; + + @Version + private Integer version; + + //@org.hibernate.annotations.Type(type = "json-hashmap") + @Column(columnDefinition = "json") + private Map props = new HashMap<>(); + + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Integer getVersion() { + return version; + } + + public Map getProps() { + return props; + } + + public void setProps(Map props) { + this.props = props; + } + + public void addProperty(String key, Object value) { + props.put(key, value); + } + + public Object getProperty(String key) { + return props.get(key); + } + } + + public static class POJO implements Serializable { + + private static final long serialVersionUID = -5009179810689351758L; + + private String first; + private String second; + + public POJO() { + } + + public POJO(String first, String second) { + this.first = first; + this.second = second; + } + + public String getFirst() { + return first; + } + + public void setFirst(String first) { + this.first = first; + } + + public String getSecond() { + return second; + } + + public void setSecond(String second) { + this.second = second; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((first == null) ? 0 : first.hashCode()); + result = prime * result + ((second == null) ? 0 : second.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + POJO other = (POJO) obj; + if (first == null) { + if (other.first != null) + return false; + } else if (!first.equals(other.first)) + return false; + if (second == null) { + if (other.second != null) + return false; + } else if (!second.equals(other.second)) + return false; + return true; + } + } + +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/OffsetDateTimeJsonTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/OffsetDateTimeJsonTest.java new file mode 100644 index 000000000..7e44b82e6 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/OffsetDateTimeJsonTest.java @@ -0,0 +1,122 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OffsetDateTimeJsonTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Test + public void test() { + OffsetDateTime dateTime = OffsetDateTime.of(2015, 10, 1, 9, 0 , 0, 0, ZoneOffset.ofHours(2)); + + doInJPA(entityManager -> { + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + location.setRentedAt(dateTime); + + Event event = new Event(); + event.setId(1L); + event.setLocation(location); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + assertEquals(dateTime, event.getLocation().getRentedAt()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + private Long id; + + @Type(JsonType.class) + @Column(columnDefinition = "jsonb") + private Location location; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + } + + public static class Location implements Serializable { + + private String country; + + private String city; + + private BigDecimal reference; + + private OffsetDateTime rentedAt; + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public BigDecimal getReference() { + return reference; + } + + public void setReference(BigDecimal reference) { + this.reference = reference; + } + + public OffsetDateTime getRentedAt() { + return rentedAt; + } + + public void setRentedAt(OffsetDateTime rentedAt) { + this.rentedAt = rentedAt; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonBinaryTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonBinaryTypeTest.java new file mode 100644 index 000000000..29d84c681 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonBinaryTypeTest.java @@ -0,0 +1,190 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.type.json.internal.JacksonUtil; +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.type.model.Location; +import com.vladmihalcea.hibernate.type.model.Ticket; +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.Session; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.sql.Statement; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OracleJsonBinaryTypeTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + Participant.class + }; + } + + private Event _event; + + private Participant _participant; + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + statement.executeUpdate( + "ALTER TABLE event MOVE LOB (location) STORE AS (CACHE)" + ); + + statement.executeUpdate( + "ALTER TABLE participant MOVE LOB (ticket, metadata) STORE AS (CACHE)" + ); + } + }); + }); + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + + Event event = new Event(); + event.setId(1L); + event.setLocation(location); + entityManager.persist(event); + + Ticket ticket = new Ticket(); + ticket.setPrice(12.34d); + ticket.setRegistrationCode("ABC123"); + + Participant participant = new Participant(); + participant.setId(1L); + participant.setTicket(ticket); + participant.setEvent(event); + participant.setMetadata(JacksonUtil.toString(location)); + + entityManager.persist(participant); + + _event = event; + _participant = participant; + }); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + assertEquals("Romania", event.getLocation().getCountry()); + assertEquals("Cluj-Napoca", event.getLocation().getCity()); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + assertEquals("Cluj-Napoca", event.getLocation().getCity()); + + Participant participant = entityManager.find(Participant.class, _participant.getId()); + assertEquals("ABC123", participant.getTicket().getRegistrationCode()); + + event.getLocation().setCity("Constanța"); + assertEquals(Integer.valueOf(0), event.getVersion()); + entityManager.flush(); + assertEquals(Integer.valueOf(1), event.getVersion()); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + event.getLocation().setCity(null); + assertEquals(Integer.valueOf(1), event.getVersion()); + entityManager.flush(); + assertEquals(Integer.valueOf(2), event.getVersion()); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + event.setLocation(null); + assertEquals(Integer.valueOf(2), event.getVersion()); + entityManager.flush(); + assertEquals(Integer.valueOf(3), event.getVersion()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event extends BaseEntity { + + @Type(JsonBlobType.class) + @Column(columnDefinition = "BLOB") + private Location location; + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + } + + @Entity(name = "Participant") + @Table(name = "participant") + public static class Participant extends BaseEntity { + + @Type(JsonBlobType.class) + @Column(columnDefinition = "BLOB") + private Ticket ticket; + + @ManyToOne + private Event event; + + @Type(JsonBlobType.class) + @Column(name = "metadata", columnDefinition = "BLOB") + private String metadata; + + public Ticket getTicket() { + return ticket; + } + + public void setTicket(Ticket ticket) { + this.ticket = ticket; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } + + public String getMetadata() { + return metadata; + } + + public void setMetadata(String metadata) { + this.metadata = metadata; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java new file mode 100644 index 000000000..d0a73a2c3 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/OracleJsonStringPropertyTest.java @@ -0,0 +1,168 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import jakarta.persistence.*; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.Session; +import org.hibernate.annotations.Check; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.query.NativeQuery; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class OracleJsonStringPropertyTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + + //@Ignore("TODO: Unsupported YET!!!") + /*doInJPA(entityManager -> { + JsonNode properties = (JsonNode) entityManager + .createNativeQuery( + "SELECT " + + " properties AS properties " + + "FROM book " + + "WHERE " + + " isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + //.addScalar("properties", new JsonStringType(JsonNode.class)) + .getSingleResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + });*/ + } + + @Test + public void testNullValue() { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties(null); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + }); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + Book book = session + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertTrue(book.getProperties().contains("\"price\": 44.99")); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonStringType.class) + @Column(columnDefinition = "VARCHAR2(1000)") + @Check(constraints = "properties IS JSON") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLGenericJsonBinaryTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLGenericJsonBinaryTypeTest.java new file mode 100644 index 000000000..02503f25a --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLGenericJsonBinaryTypeTest.java @@ -0,0 +1,138 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.type.model.Location; +import com.vladmihalcea.hibernate.type.model.Ticket; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLGenericJsonBinaryTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + Participant.class + }; + } + + private Event _event; + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Location cluj = new Location(); + cluj.setCountry("Romania"); + cluj.setCity("Cluj-Napoca"); + + Location newYork = new Location(); + newYork.setCountry("US"); + newYork.setCity("New-York"); + + Location london = new Location(); + london.setCountry("UK"); + london.setCity("London"); + + Event event = new Event(); + event.setId(1L); + event.setLocation(cluj); + event.setAlternativeLocations(Arrays.asList(newYork, london)); + + entityManager.persist(event); + + _event = event; + }); + } + + @Test + public void test() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + + assertEquals("Cluj-Napoca", event.getLocation().getCity()); + + assertEquals(2, event.getAlternativeLocations().size()); + assertEquals("New-York", event.getAlternativeLocations().get(0).getCity()); + assertEquals("London", event.getAlternativeLocations().get(1).getCity()); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private Location location; + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private List alternativeLocations; + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + + public List getAlternativeLocations() { + return alternativeLocations; + } + + public void setAlternativeLocations(List alternativeLocations) { + this.alternativeLocations = alternativeLocations; + } + } + + @Entity(name = "Participant") + @Table(name = "participant") + public static class Participant extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private Ticket ticket; + + @ManyToOne + private Event event; + + public Ticket getTicket() { + return ticket; + } + + public void setTicket(Ticket ticket) { + this.ticket = ticket; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeLazyGroupTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeLazyGroupTest.java new file mode 100644 index 000000000..00841e8d5 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeLazyGroupTest.java @@ -0,0 +1,70 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.type.model.Event; +import com.vladmihalcea.hibernate.type.model.Location; +import com.vladmihalcea.hibernate.type.model.Participant; +import com.vladmihalcea.hibernate.type.model.Ticket; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonBinaryTypeLazyGroupTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + Participant.class + }; + } + + @Test + public void test() { + final AtomicReference eventHolder = new AtomicReference<>(); + final AtomicReference participantHolder = new AtomicReference<>(); + + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + + Event event = new Event(); + event.setId(1L); + event.setLocation(location); + entityManager.persist(event); + + Ticket ticket = new Ticket(); + ticket.setPrice(12.34d); + ticket.setRegistrationCode("ABC123"); + + Participant participant = new Participant(); + participant.setId(1L); + participant.setTicket(ticket); + participant.setEvent(event); + + entityManager.persist(participant); + + eventHolder.set(event); + participantHolder.set(participant); + }); + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, eventHolder.get().getId()); + LOGGER.debug("Fetched event"); + assertEquals("Cluj-Napoca", event.getLocation().getCity()); + + Participant participant = entityManager.find(Participant.class, participantHolder.get().getId()); + LOGGER.debug("Fetched participant"); + assertEquals("ABC123", participant.getTicket().getRegistrationCode()); + }); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeNestedCollectionExplicitEqualsTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeNestedCollectionExplicitEqualsTest.java new file mode 100644 index 000000000..1d9cb7158 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeNestedCollectionExplicitEqualsTest.java @@ -0,0 +1,129 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonBinaryTypeNestedCollectionExplicitEqualsTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + private Post _post; + + @Override + protected void afterInit() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + List attributes = new ArrayList<>(); + attributes.add("JPA"); + attributes.add("Hibernate"); + + PostAttributes customAttributes = new PostAttributes(); + customAttributes.setAttributes(attributes); + + Post post = new Post(); + post.setCustomAttributes(customAttributes); + + entityManager.persist(post); + + _post = post; + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getInsert()); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, _post.getId()); + List attributes = post.getCustomAttributes().getAttributes(); + + assertTrue(attributes.contains("JPA")); + assertTrue(attributes.contains("Hibernate")); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private PostAttributes customAttributes; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public PostAttributes getCustomAttributes() { + return customAttributes; + } + + public void setCustomAttributes(PostAttributes customAttributes) { + this.customAttributes = customAttributes; + } + } + + public static class PostAttributes { + + private List attributes; + + public List getAttributes() { + return attributes; + } + + public void setAttributes(List attributes) { + this.attributes = attributes; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostAttributes)) return false; + PostAttributes that = (PostAttributes) o; + return Objects.equals(attributes, that.attributes); + } + + @Override + public int hashCode() { + return Objects.hash(attributes); + } + } + +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeNestedCollectionNoEqualsTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeNestedCollectionNoEqualsTest.java new file mode 100644 index 000000000..42dd7b884 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeNestedCollectionNoEqualsTest.java @@ -0,0 +1,115 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonBinaryTypeNestedCollectionNoEqualsTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + private Post _post; + + @Override + protected void afterInit() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + List attributes = new ArrayList<>(); + attributes.add("JPA"); + attributes.add("Hibernate"); + + PostAttributes customAttributes = new PostAttributes(); + customAttributes.setAttributes(attributes); + + Post post = new Post(); + post.setCustomAttributes(customAttributes); + + entityManager.persist(post); + + _post = post; + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getInsert()); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, _post.getId()); + List attributes = post.getCustomAttributes().getAttributes(); + + assertTrue(attributes.contains("JPA")); + assertTrue(attributes.contains("Hibernate")); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private PostAttributes customAttributes; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public PostAttributes getCustomAttributes() { + return customAttributes; + } + + public void setCustomAttributes(PostAttributes customAttributes) { + this.customAttributes = customAttributes; + } + } + + public static class PostAttributes { + + private List attributes; + + public List getAttributes() { + return attributes; + } + + public void setAttributes(List attributes) { + this.attributes = attributes; + } + } + +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeSetTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeSetTest.java new file mode 100644 index 000000000..057a2b2a8 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeSetTest.java @@ -0,0 +1,197 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.beans.ConstructorProperties; +import java.io.Serializable; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; + +/** + * @author Sergei Poznanski + */ +public class PostgreSQLJsonBinaryTypeSetTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + User.class + }; + } + + private User _user; + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + User user = new User(); + + user.setId(1L); + user.setPhones(new HashSet<>(asList("7654321", "1234567"))); + user.setRoles(EnumSet.of(Role.ADMIN, Role.USER)); + user.setChildren(new HashSet<>(asList( + new Child("Jane", 2, new HashSet<>(asList("toy1", "toy2"))), + new Child("John", 1, new HashSet<>(asList("toy3", "toy4"))) + ))); + + entityManager.persist(user); + _user = user; + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + User user = entityManager.find(User.class, _user.getId()); + + QueryCountHolder.clear(); + + user.setPhones(new HashSet<>(asList("1592637", "9518473"))); + user.setRoles(EnumSet.of(Role.USER, Role.DEV)); + user.setChildren(new HashSet<>(asList( + new Child("Jinny", 1, new HashSet<>(asList("toy5", "toy6"))), + new Child("Jenny", 2, new HashSet<>(asList("toy7", "toy8"))) + ))); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + + doInJPA(entityManager -> { + User user = entityManager.find(User.class, _user.getId()); + assertEquals(new HashSet<>(asList("9518473", "1592637")), user.getPhones()); + assertEquals(EnumSet.of(Role.DEV, Role.USER), user.getRoles()); + assertEquals(new HashSet<>(asList( + new Child("Jenny", 2, new HashSet<>(asList("toy8", "toy7"))), + new Child("Jinny", 1, new HashSet<>(asList("toy6", "toy5"))) + )), user.getChildren()); + assertEquals(Integer.valueOf(1), user.getVersion()); + }); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + User user = entityManager.find(User.class, _user.getId()); + assertEquals(new HashSet<>(asList("1234567", "7654321")), user.getPhones()); + assertEquals(EnumSet.of(Role.USER, Role.ADMIN), user.getRoles()); + assertEquals(new HashSet<>(asList( + new Child("John", 1, new HashSet<>(asList("toy4", "toy3"))), + new Child("Jane", 2, new HashSet<>(asList("toy2", "toy1"))) + )), user.getChildren()); + assertEquals(Integer.valueOf(0), user.getVersion()); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Entity + @Table(name = "users") + @DynamicUpdate + public static class User extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private Set phones; + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private EnumSet roles; + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private Set children; + + public Set getPhones() { + return phones; + } + + public void setPhones(Set phones) { + this.phones = phones; + } + + public EnumSet getRoles() { + return roles; + } + + public void setRoles(EnumSet roles) { + this.roles = roles; + } + + public Set getChildren() { + return children; + } + + public void setChildren(final Set children) { + this.children = children; + } + } + + public enum Role { + USER, ADMIN, DEV + } + + public static class Child implements Serializable { + + private final String name; + private final Integer age; + private final Set toys; + + @ConstructorProperties({"name", "age", "toys"}) + public Child(String name, Integer age, final Set toys) { + this.name = Objects.requireNonNull(name); + this.age = Objects.requireNonNull(age); + this.toys = Objects.requireNonNull(toys); + } + + public String getName() { + return name; + } + + public Integer getAge() { + return age; + } + + public Set getToys() { + return toys; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Child child = (Child) o; + if (!name.equals(child.name)) return false; + if (!age.equals(child.age)) return false; + return toys.equals(child.toys); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + age.hashCode(); + result = 31 * result + toys.hashCode(); + return result; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeTest.java new file mode 100644 index 000000000..76e1eb6cc --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonBinaryTypeTest.java @@ -0,0 +1,193 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.type.json.internal.JacksonUtil; +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.type.model.Location; +import com.vladmihalcea.hibernate.type.model.Ticket; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonBinaryTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + Participant.class + }; + } + + private Event _event; + + private Participant _participant; + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + + Event event = new Event(); + event.setId(1L); + event.setLocation(location); + entityManager.persist(event); + + Ticket ticket = new Ticket(); + ticket.setPrice(12.34d); + ticket.setRegistrationCode("ABC123"); + + Participant participant = new Participant(); + participant.setId(1L); + participant.setTicket(ticket); + participant.setEvent(event); + participant.setMetaData(JacksonUtil.toString(location)); + + entityManager.persist(participant); + + _event = event; + _participant = participant; + }); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + assertEquals("Romania", event.getLocation().getCountry()); + assertEquals("Cluj-Napoca", event.getLocation().getCity()); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Test + public void test() { + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + assertEquals("Cluj-Napoca", event.getLocation().getCity()); + + Participant participant = entityManager.find(Participant.class, _participant.getId()); + assertEquals("ABC123", participant.getTicket().getRegistrationCode()); + + List participants = entityManager.createNativeQuery( + "select jsonb_pretty(p.ticket) " + + "from participant p " + + "where p.ticket ->> 'price' > :price") + .setParameter("price", "10") + .getResultList(); + + List countries = entityManager.createNativeQuery( + "select p.metadata ->> 'country' " + + "from participant p ") + .getResultList(); + + event.getLocation().setCity("Constanța"); + assertEquals(Integer.valueOf(0), event.getVersion()); + entityManager.flush(); + assertEquals(Integer.valueOf(1), event.getVersion()); + + assertEquals(1, participants.size()); + assertEquals(1, countries.size()); + assertNotNull(countries.get(0)); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + event.getLocation().setCity(null); + assertEquals(Integer.valueOf(1), event.getVersion()); + entityManager.flush(); + assertEquals(Integer.valueOf(2), event.getVersion()); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + event.setLocation(null); + assertEquals(Integer.valueOf(2), event.getVersion()); + entityManager.flush(); + assertEquals(Integer.valueOf(3), event.getVersion()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private Location location; + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + } + + @Entity(name = "Participant") + @Table(name = "participant") + public static class Participant extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private Ticket ticket; + + @ManyToOne + private Event event; + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private String metaData; + + public Ticket getTicket() { + return ticket; + } + + public void setTicket(Ticket ticket) { + this.ticket = ticket; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } + + public String getMetaData() { + return metaData; + } + + public void setMetaData(String metaData) { + this.metaData = metaData; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonListEnumTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonListEnumTest.java new file mode 100644 index 000000000..c4aaaa9df --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonListEnumTest.java @@ -0,0 +1,93 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonListEnumTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .addProperty(PropertyType.BEST_SELLER) + .addProperty(PropertyType.FREE_CHAPTER) + ); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + List bookProperties = book.getProperties(); + assertEquals(2, bookProperties.size()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private List propertyTypes = new ArrayList<>(); + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public List getProperties() { + return propertyTypes; + } + + public Book setProperties(List properties) { + this.propertyTypes = properties; + return this; + } + + public Book addProperty(PropertyType propertyType) { + propertyTypes.add(propertyType); + return this; + } + } + + public enum PropertyType { + BEST_SELLER, + FREE_CHAPTER + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonListPojoTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonListPojoTest.java new file mode 100644 index 000000000..d84415129 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonListPojoTest.java @@ -0,0 +1,120 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonListPojoTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .addProperty("title", "High-Performance Java Persistence") + .addProperty("author", "Vlad Mihalcea") + .addProperty("publisher", "Amazon") + .addProperty("price", "$44.95") + ); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + List bookProperties = book.getProperties(); + assertEquals(4, bookProperties.size()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private List properties = new ArrayList<>(); + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public List getProperties() { + return properties; + } + + public Book setProperties(List properties) { + this.properties = properties; + return this; + } + + public Book addProperty(String key, String value) { + properties.add(new Property(key, value)); + return this; + } + } + + public static class Property { + + private String key; + private String value; + + public Property() { + } + + public Property(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonMapTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonMapTest.java new file mode 100644 index 000000000..c1b8e00d8 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonMapTest.java @@ -0,0 +1,99 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonMapTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .addProperty("title", "High-Performance Java Persistence") + .addProperty("author", "Vlad Mihalcea") + .addProperty("publisher", "Amazon") + .addProperty("price", "$44.95") + ); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + Map bookProperties = book.getProperties(); + + assertEquals( + "High-Performance Java Persistence", + bookProperties.get("title") + ); + + assertEquals( + "Vlad Mihalcea", + bookProperties.get("author") + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "jsonb") + private Map properties = new HashMap<>(); + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public Map getProperties() { + return properties; + } + + public Book setProperties(Map properties) { + this.properties = properties; + return this; + } + + public Book addProperty(String key, String value) { + properties.put(key, value); + return this; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonNodeBinaryTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonNodeBinaryTypeTest.java new file mode 100644 index 000000000..396ea2960 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonNodeBinaryTypeTest.java @@ -0,0 +1,135 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.json.internal.JacksonUtil; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonNodeBinaryTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setProperties( + JacksonUtil.toJsonNode( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + + entityManager.persist(book); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + Book book = session + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties( + JacksonUtil.toJsonNode( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ) + ); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + Book book = session + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(expectedPrice(), book.getProperties().get("price").asText()); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + protected String initialPrice() { + return "44.99"; + } + + protected String expectedPrice() { + return "44.99"; + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonNodeBinaryType.class) + @Column(columnDefinition = "jsonb") + private JsonNode properties; + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public JsonNode getProperties() { + return properties; + } + + public void setProperties(JsonNode properties) { + this.properties = properties; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonNodeTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonNodeTypeTest.java new file mode 100644 index 000000000..7d6b394dd --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonNodeTypeTest.java @@ -0,0 +1,235 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.json.internal.JacksonUtil; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.boot.spi.MetadataBuilderContributor; +import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; +import org.hibernate.query.NativeQuery; +import org.hibernate.transform.AliasToBeanResultTransformer; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonNodeTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + EntityManagerFactoryBuilderImpl.METADATA_BUILDER_CONTRIBUTOR, + (MetadataBuilderContributor) metadataBuilder -> metadataBuilder.applyBasicType( + JsonNodeBinaryType.INSTANCE + ) + ); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setProperties( + JacksonUtil.toJsonNode( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + + entityManager.persist(book); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + Book book = session + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties( + JacksonUtil.toJsonNode( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ) + ); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + Book book = session + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(expectedPrice(), book.getProperties().get("price").asText()); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Test + public void testNativeQueryResultTransformer() { + doInJPA(entityManager -> { + List books = entityManager.createNativeQuery( + "SELECT " + + " b.id as id, " + + " b.properties as properties " + + "FROM book b") + .unwrap(NativeQuery.class) + .setResultTransformer(new AliasToBeanResultTransformer(BookDTO.class)) + .getResultList(); + + assertEquals(1, books.size()); + BookDTO book = books.get(0); + + assertEquals(expectedPrice(), book.getProperties().get("price").asText()); + }); + } + + @Test + @Ignore("TODO: Unsupported YET!!!") + public void testNativeQueryResultMapping() { + doInJPA(entityManager -> { + List books = entityManager.createNativeQuery( + "SELECT " + + " b.id as id, " + + " b.properties as properties " + + "FROM book b") + .unwrap(NativeQuery.class) + //.setResultSetMapping("BookDTO") + .getResultList(); + + assertEquals(1, books.size()); + BookDTO book = books.get(0); + + assertEquals(expectedPrice(), book.getProperties().get("price").asText()); + }); + } + + protected String initialPrice() { + return "44.99"; + } + + protected String expectedPrice() { + return "44.99"; + } + + public static class BookDTO { + + private long id; + + private JsonNode properties; + + public BookDTO() { + } + + public BookDTO(Number id, JsonNode properties) { + this.id = id.longValue(); + this.properties = properties; + } + + public Long getId() { + return id; + } + + public void setId(Number id) { + this.id = id.longValue(); + } + + public JsonNode getProperties() { + return properties; + } + + public void setProperties(JsonNode properties) { + this.properties = properties; + } + } + + @Entity(name = "Book") + @Table(name = "book") + @SqlResultSetMapping( + name = "BookDTO", + classes = { + @ConstructorResult( + targetClass = BookDTO.class, + columns = { + @ColumnResult(name = "id"), + @ColumnResult(name = "properties", type = JsonNode.class), + } + ) + } + ) + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonNodeBinaryType.class) + @Column(columnDefinition = "jsonb") + private JsonNode properties; + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public JsonNode getProperties() { + return properties; + } + + public void setProperties(JsonNode properties) { + this.properties = properties; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonParameterizedPropertyTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonParameterizedPropertyTest.java new file mode 100644 index 000000000..89d5381e8 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonParameterizedPropertyTest.java @@ -0,0 +1,167 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonParameterizedPropertyTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + + //@Ignore("TODO: Unsupported YET!!!") + /*doInJPA(entityManager -> { + JsonNode properties = (JsonNode) entityManager + .createNativeQuery( + "SELECT " + + " properties AS properties " + + "FROM book " + + "WHERE " + + " isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + //.addScalar("properties", new JsonBinaryType(JsonNode.class)) + .getSingleResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + });*/ + } + + @Test + public void testNullValue() { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties(null); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + }); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + Book book = session + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertTrue(book.getProperties().contains("\"price\": 44.99")); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @MappedSuperclass + public static class AbstractBook { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private T properties; + + public T getProperties() { + return properties; + } + + public AbstractBook setProperties(T properties) { + this.properties = properties; + return this; + } + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book extends AbstractBook { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonStringPropertyTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonStringPropertyTest.java new file mode 100644 index 000000000..c92964efa --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonStringPropertyTest.java @@ -0,0 +1,190 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.fasterxml.jackson.core.JsonParseException; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hibernate.util.ExceptionUtil; +import jakarta.persistence.*; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonStringPropertyTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + + //@Ignore("TODO: Unsupported YET!!!") + /*doInJPA(entityManager -> { + JsonNode properties = (JsonNode) entityManager + .createNativeQuery( + "SELECT " + + " properties AS properties " + + "FROM book " + + "WHERE " + + " isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + //.addScalar("properties", new JsonBinaryType(JsonNode.class)) + .getSingleResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + });*/ + } + + @Test + public void testNullValue() { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties(null); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + }); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + Book book = session + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertTrue(book.getProperties().contains("\"price\": 44.99")); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Test + public void testInvalidJson() { + try { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": " + + " \"extras \": [" + + "}" + ); + }); + fail("An invalid JSON should throw an exception!"); + } catch (Exception e) { + JsonParseException rootCause = ExceptionUtil.rootCause(e); + assertTrue(rootCause.getMessage().contains("Unexpected character (':'")); + } + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonStringTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonStringTypeTest.java new file mode 100644 index 000000000..bc2bb80f9 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/PostgreSQLJsonStringTypeTest.java @@ -0,0 +1,143 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.type.model.Location; +import com.vladmihalcea.hibernate.type.model.Ticket; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonStringTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + Participant.class + }; + } + + private Event _event; + + private Participant _participant; + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + + Event event = new Event(); + event.setId(1L); + event.setLocation(location); + entityManager.persist(event); + + Ticket ticket = new Ticket(); + ticket.setPrice(12.34d); + ticket.setRegistrationCode("ABC123"); + + Participant participant = new Participant(); + participant.setId(1L); + participant.setTicket(ticket); + participant.setEvent(event); + + entityManager.persist(participant); + + _event = event; + _participant = participant; + }); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + assertEquals("Cluj-Napoca", event.getLocation().getCity()); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + + Participant participant = entityManager.find(Participant.class, _participant.getId()); + assertEquals("ABC123", participant.getTicket().getRegistrationCode()); + + List participants = entityManager.createNativeQuery( + "select p.ticket ->>'registrationCode' " + + "from participant p " + + "where p.ticket ->> 'price' > '10'") + .getResultList(); + + event.getLocation().setCity("Constanța"); + entityManager.flush(); + + assertEquals(1, participants.size()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "json") + private Location location; + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + } + + @Entity(name = "Participant") + @Table(name = "participant") + public static class Participant extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "json") + private Ticket ticket; + + @ManyToOne + private Event event; + + public Ticket getTicket() { + return ticket; + } + + public void setTicket(Ticket ticket) { + this.ticket = ticket; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } + } + +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/SQLServerJsonStringPropertyTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/SQLServerJsonStringPropertyTest.java new file mode 100644 index 000000000..5966eb89d --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/SQLServerJsonStringPropertyTest.java @@ -0,0 +1,165 @@ +package com.vladmihalcea.hibernate.type.json; + +import com.vladmihalcea.hibernate.util.AbstractSQLServerIntegrationTest; +import jakarta.persistence.*; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.Session; +import org.hibernate.annotations.Check; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerJsonStringPropertyTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + + //@Ignore("TODO: Unsupported YET!!!") + /*doInJPA(entityManager -> { + JsonNode properties = (JsonNode) entityManager + .createNativeQuery( + "SELECT " + + " properties AS properties " + + "FROM book " + + "WHERE " + + " isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + //.addScalar("properties", new JsonStringType(JsonNode.class)) + .getSingleResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + });*/ + } + + @Test + public void testNullValue() { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties(null); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getUpdate()); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + }); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + Book book = session + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertTrue(book.getProperties().contains("\"price\": 44.99")); + }); + + QueryCount queryCount = QueryCountHolder.getGrandTotal(); + assertEquals(1, queryCount.getTotal()); + assertEquals(1, queryCount.getSelect()); + assertEquals(0, queryCount.getUpdate()); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "NVARCHAR(1000)") + @Check(constraints = "ISJSON(properties) = 1") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/CustomJsonSerializer.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/CustomJsonSerializer.java new file mode 100644 index 000000000..47b06e915 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/CustomJsonSerializer.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hibernate.type.json.configuration; + +import com.vladmihalcea.hibernate.type.json.internal.JacksonUtil; +import com.vladmihalcea.hibernate.type.util.JsonSerializer; + +/** + * @author Vlad Mihalcea + */ +public class CustomJsonSerializer implements JsonSerializer { + + private static boolean called; + + public static boolean isCalled() { + return called; + } + + public static void reset() { + called = false; + } + + @Override + public T clone(T value) { + called = true; + return JacksonUtil.clone(value); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/CustomJsonSerializerSupplier.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/CustomJsonSerializerSupplier.java new file mode 100644 index 000000000..934666b5c --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/CustomJsonSerializerSupplier.java @@ -0,0 +1,15 @@ +package com.vladmihalcea.hibernate.type.json.configuration; + +import com.vladmihalcea.hibernate.type.util.JsonSerializer; +import com.vladmihalcea.hibernate.type.util.JsonSerializerSupplier; + +/** + * @author Vlad Mihalcea + */ +public class CustomJsonSerializerSupplier implements JsonSerializerSupplier { + + @Override + public JsonSerializer get() { + return new CustomJsonSerializer(); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/CustomObjectMapperSupplier.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/CustomObjectMapperSupplier.java new file mode 100644 index 000000000..6c4f5d1ee --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/CustomObjectMapperSupplier.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hibernate.type.json.configuration; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.vladmihalcea.hibernate.type.util.ObjectMapperSupplier; + +import java.util.TimeZone; + +/** + * @author Vlad Mihalcea + */ +public class CustomObjectMapperSupplier implements ObjectMapperSupplier { + + @Override + public ObjectMapper get() { + ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT")); + SimpleModule simpleModule = new SimpleModule("SimpleModule", new Version(1, 0, 0, null, null, null)); + simpleModule.addSerializer(new MoneySerializer()); + objectMapper.registerModule(simpleModule); + return objectMapper; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/MoneySerializer.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/MoneySerializer.java new file mode 100644 index 000000000..f8e052b0f --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/MoneySerializer.java @@ -0,0 +1,25 @@ +package com.vladmihalcea.hibernate.type.json.configuration; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.math.BigDecimal; + +/** + * @author Vlad Mihalcea + */ +public class MoneySerializer extends JsonSerializer { + + @Override + public void serialize(BigDecimal value, JsonGenerator jsonGenerator, SerializerProvider provider) + throws IOException { + jsonGenerator.writeString(value.setScale(2, BigDecimal.ROUND_HALF_UP).toString()); + } + + @Override + public Class handledType() { + return BigDecimal.class; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/PostgreSQLJsonBinaryTypeConfigurationJvmForkTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/PostgreSQLJsonBinaryTypeConfigurationJvmForkTest.java new file mode 100644 index 000000000..c9f06c4e1 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/PostgreSQLJsonBinaryTypeConfigurationJvmForkTest.java @@ -0,0 +1,114 @@ +package com.vladmihalcea.hibernate.type.json.configuration; + +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.io.Serializable; +import java.math.BigDecimal; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonBinaryTypeConfigurationJvmForkTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + public void init() { + System.setProperty( + Configuration.PROPERTIES_FILE_PATH, + "PostgreSQLJsonBinaryTypeConfigurationTest.properties" + ); + super.init(); + } + + @Override + public void destroy() { + super.destroy(); + System.getProperties().remove(Configuration.PROPERTIES_FILE_PATH); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + location.setReference(BigDecimal.valueOf(2.25262562526626D)); + + Event event = new Event(); + event.setId(1L); + event.setLocation(location); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + assertEquals("2.25", event.getLocation().getReference().toString()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private Location location; + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + } + + public static class Location implements Serializable { + + private String country; + + private String city; + + private BigDecimal reference; + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public BigDecimal getReference() { + return reference; + } + + public void setReference(BigDecimal reference) { + this.reference = reference; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/PostgreSQLJsonBinaryTypeCustomJsonSerializerConfigurationJvmForkTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/PostgreSQLJsonBinaryTypeCustomJsonSerializerConfigurationJvmForkTest.java new file mode 100644 index 000000000..8204e82c2 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/PostgreSQLJsonBinaryTypeCustomJsonSerializerConfigurationJvmForkTest.java @@ -0,0 +1,122 @@ +package com.vladmihalcea.hibernate.type.json.configuration; + +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import com.vladmihalcea.hibernate.type.util.Configuration; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.io.Serializable; +import java.math.BigDecimal; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonBinaryTypeCustomJsonSerializerConfigurationJvmForkTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + public void init() { + System.setProperty( + Configuration.PROPERTIES_FILE_PATH, + "PostgreSQLJsonBinaryTypeCustomJsonSerializerConfigurationTest.properties" + ); + super.init(); + } + + @Override + public void destroy() { + super.destroy(); + System.getProperties().remove(Configuration.PROPERTIES_FILE_PATH); + } + + @Test + public void test() { + assertFalse(CustomJsonSerializer.isCalled()); + + doInJPA(entityManager -> { + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + location.setReference(BigDecimal.valueOf(2.25262562526626D)); + + Event event = new Event(); + event.setId(1L); + event.setLocation(location); + entityManager.persist(event); + }); + + assertTrue(CustomJsonSerializer.isCalled()); + CustomJsonSerializer.reset(); + assertFalse(CustomJsonSerializer.isCalled()); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + assertEquals("2.25262562526626", event.getLocation().getReference().toString()); + }); + + assertTrue(CustomJsonSerializer.isCalled()); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private Location location; + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + } + + public static class Location implements Serializable { + + private String country; + + private String city; + + private BigDecimal reference; + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public BigDecimal getReference() { + return reference; + } + + public void setReference(BigDecimal reference) { + this.reference = reference; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/PostgreSQLJsonBinaryTypeProgrammaticConfigurationSupplierTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/PostgreSQLJsonBinaryTypeProgrammaticConfigurationSupplierTest.java new file mode 100644 index 000000000..130b4f235 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/PostgreSQLJsonBinaryTypeProgrammaticConfigurationSupplierTest.java @@ -0,0 +1,136 @@ +package com.vladmihalcea.hibernate.type.json.configuration; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.junit.Test; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Properties; +import java.util.TimeZone; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonBinaryTypeProgrammaticConfigurationSupplierTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT")); + SimpleModule simpleModule = new SimpleModule("SimpleModule", new Version(1, 0, 0, null, null, null)); + simpleModule.addSerializer(new MoneySerializer()); + objectMapper.registerModule(simpleModule); + + JsonBinaryType jsonBinaryType = new JsonBinaryType(objectMapper, Location.class); + + properties.put("hibernate.type_contributors", + (TypeContributorList) () -> Collections.singletonList( + (typeContributions, serviceRegistry) -> + typeContributions.contributeType( + jsonBinaryType, "location" + ) + ) + ); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + location.setReference(BigDecimal.valueOf(2.25262562526626D)); + + Event event = new Event(); + event.setId(1L); + event.setLocation(location); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + assertEquals("2.25", event.getLocation().getReference().toString()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + private Long id; + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private Location location; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + } + + public static class Location implements Serializable { + + private String country; + + private String city; + + private BigDecimal reference; + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public BigDecimal getReference() { + return reference; + } + + public void setReference(BigDecimal reference) { + this.reference = reference; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/PostgreSQLJsonBinaryTypeProgrammaticConfigurationTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/PostgreSQLJsonBinaryTypeProgrammaticConfigurationTest.java new file mode 100644 index 000000000..5e58d7751 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/configuration/PostgreSQLJsonBinaryTypeProgrammaticConfigurationTest.java @@ -0,0 +1,127 @@ +package com.vladmihalcea.hibernate.type.json.configuration; + +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.junit.Test; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonBinaryTypeProgrammaticConfigurationTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + CustomObjectMapperSupplier customObjectMapperSupplier = new CustomObjectMapperSupplier(); + JsonBinaryType jsonBinaryType = new JsonBinaryType(customObjectMapperSupplier.get(), Location.class); + + properties.put("hibernate.type_contributors", + (TypeContributorList) () -> Collections.singletonList( + (typeContributions, serviceRegistry) -> + typeContributions.contributeType( + jsonBinaryType, "location" + ) + ) + ); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + location.setReference(BigDecimal.valueOf(2.25262562526626D)); + + Event event = new Event(); + event.setId(1L); + event.setLocation(location); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + assertEquals("2.25", event.getLocation().getReference().toString()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + private Long id; + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private Location location; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + } + + public static class Location implements Serializable { + + private String country; + + private String city; + + private BigDecimal reference; + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public BigDecimal getReference() { + return reference; + } + + public void setReference(BigDecimal reference) { + this.reference = reference; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericH2JsonMapTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericH2JsonMapTest.java new file mode 100644 index 000000000..a95bcfb1f --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericH2JsonMapTest.java @@ -0,0 +1,106 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.util.AbstractTest; +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.H2DataSourceProvider; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class GenericH2JsonMapTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new H2DataSourceProvider(); + } + + @Test + public void test() { + + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .addProperty("title", "High-Performance Java Persistence") + .addProperty("author", "Vlad Mihalcea") + .addProperty("publisher", "Amazon") + .addProperty("price", "$44.95") + ); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + Map bookProperties = book.getProperties(); + + assertEquals( + "High-Performance Java Persistence", + bookProperties.get("title") + ); + + assertEquals( + "Vlad Mihalcea", + bookProperties.get("author") + ); + }); + } + + @Entity(name = "Book") + public static class Book { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "json") + private Map properties = new HashMap<>(); + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public Map getProperties() { + return properties; + } + + public Book setProperties(Map properties) { + this.properties = properties; + return this; + } + + public Book addProperty(String key, String value) { + properties.put(key, value); + return this; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericMySQLJsonMapTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericMySQLJsonMapTest.java new file mode 100644 index 000000000..efc4e5c52 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericMySQLJsonMapTest.java @@ -0,0 +1,100 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class GenericMySQLJsonMapTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .addProperty("title", "High-Performance Java Persistence") + .addProperty("author", "Vlad Mihalcea") + .addProperty("publisher", "Amazon") + .addProperty("price", "$44.95") + ); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + Map bookProperties = book.getProperties(); + + assertEquals( + "High-Performance Java Persistence", + bookProperties.get("title") + ); + + assertEquals( + "Vlad Mihalcea", + bookProperties.get("author") + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "json") + private Map properties = new HashMap<>(); + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public Map getProperties() { + return properties; + } + + public Book setProperties(Map properties) { + this.properties = properties; + return this; + } + + public Book addProperty(String key, String value) { + properties.put(key, value); + return this; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java new file mode 100644 index 000000000..668dd4f23 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericOracleJsonMapTest.java @@ -0,0 +1,102 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.util.AbstractOracleIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.Check; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class GenericOracleJsonMapTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .addProperty("title", "High-Performance Java Persistence") + .addProperty("author", "Vlad Mihalcea") + .addProperty("publisher", "Amazon") + .addProperty("price", "$44.95") + ); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + Map bookProperties = book.getProperties(); + + assertEquals( + "High-Performance Java Persistence", + bookProperties.get("title") + ); + + assertEquals( + "Vlad Mihalcea", + bookProperties.get("author") + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "VARCHAR2(1000)") + @Check(constraints = "properties IS JSON") + private Map properties = new HashMap<>(); + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public Map getProperties() { + return properties; + } + + public Book setProperties(Map properties) { + this.properties = properties; + return this; + } + + public Book addProperty(String key, String value) { + properties.put(key, value); + return this; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericPostgreSQLJsonMapTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericPostgreSQLJsonMapTest.java new file mode 100644 index 000000000..284f12ce9 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericPostgreSQLJsonMapTest.java @@ -0,0 +1,100 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class GenericPostgreSQLJsonMapTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .addProperty("title", "High-Performance Java Persistence") + .addProperty("author", "Vlad Mihalcea") + .addProperty("publisher", "Amazon") + .addProperty("price", "$44.95") + ); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + Map bookProperties = book.getProperties(); + + assertEquals( + "High-Performance Java Persistence", + bookProperties.get("title") + ); + + assertEquals( + "Vlad Mihalcea", + bookProperties.get("author") + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "jsonb") + private Map properties = new HashMap<>(); + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public Map getProperties() { + return properties; + } + + public Book setProperties(Map properties) { + this.properties = properties; + return this; + } + + public Book addProperty(String key, String value) { + properties.put(key, value); + return this; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericPostgreSQLJsonStringTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericPostgreSQLJsonStringTest.java new file mode 100644 index 000000000..a5df7de71 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericPostgreSQLJsonStringTest.java @@ -0,0 +1,162 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.query.NativeQuery; +import org.hibernate.type.BasicTypeReference; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class GenericPostgreSQLJsonStringTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99" + + "}" + ) + ); + }); + } + + @Test + @Ignore("TODO: Unsupported YET!!!") + public void test() { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties( + "{" + + " \"title\": \"High-Performance Java Persistence\"," + + " \"author\": \"Vlad Mihalcea\"," + + " \"publisher\": \"Amazon\"," + + " \"price\": 44.99," + + " \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" + + "}" + ); + }); + + //TODO: Unsupported!!! Got to find a way to pass the Type to the scalar method. + /* + doInJPA(entityManager -> { + JsonType jsonType = new JsonType(JsonNode.class); + JsonNode properties = (JsonNode) entityManager.createNativeQuery( + "SELECT " + + " properties AS properties " + + "FROM book " + + "WHERE " + + " isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + .addScalar("properties", new BasicTypeReference( + jsonType.getName(), + jsonType.getJavaTypeDescriptor(), + jsonType.getSqlType() + )) + .getSingleResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + });*/ + } + + @Test + public void testNullValue() { + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + QueryCountHolder.clear(); + + book.setProperties(null); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties()); + }); + } + + @Test + public void testLoad() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + Book book = session + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertTrue(book.getProperties().contains("\"price\": 44.99")); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "jsonb") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericSQLServerJsonMapTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericSQLServerJsonMapTest.java new file mode 100644 index 000000000..2a5bf82f9 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/generic/GenericSQLServerJsonMapTest.java @@ -0,0 +1,103 @@ +package com.vladmihalcea.hibernate.type.json.generic; + +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.util.AbstractSQLServerIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.Check; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class GenericSQLServerJsonMapTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .addProperty("title", "High-Performance Java Persistence") + .addProperty("author", "Vlad Mihalcea") + .addProperty("publisher", "Amazon") + .addProperty("price", "$44.95") + ); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + Map bookProperties = book.getProperties(); + + assertEquals( + "High-Performance Java Persistence", + bookProperties.get("title") + ); + + assertEquals( + "Vlad Mihalcea", + bookProperties.get("author") + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "NVARCHAR(1000)") + @Check(constraints = "ISJSON(properties) = 1") + private Map properties = new HashMap<>(); + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public Map getProperties() { + return properties; + } + + public Book setProperties(Map properties) { + this.properties = properties; + return this; + } + + public Book addProperty(String key, String value) { + properties.put(key, value); + return this; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/internal/JacksonUtilTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/internal/JacksonUtilTest.java new file mode 100644 index 000000000..dcb5bcc0f --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/internal/JacksonUtilTest.java @@ -0,0 +1,168 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import org.junit.Test; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Currency; +import java.util.List; +import java.util.Objects; + +import static org.junit.Assert.assertEquals; + +public class JacksonUtilTest { + + @Test + public void cloneDeserializeStepErrorTest() { + MyEntity entity = new MyEntity(); + + entity.setValue("some value"); + entity.setPojos(Arrays.asList( + createMyPojo("first value", MyType.A, "1.1", createOtherPojo("USD")), + createMyPojo("second value", MyType.B, "1.2", createOtherPojo("BRL")) + )); + + MyEntity clone = JacksonUtil.clone(entity); + assertEquals(clone, entity); + + List clonePojos = JacksonUtil.clone(entity.getPojos()); + assertEquals(clonePojos, entity.getPojos()); + } + + private MyPojo createMyPojo(String value, MyType myType, String number, OtherPojo otherPojo) { + MyPojo myPojo = new MyPojo(); + myPojo.setValue(value); + myPojo.setType(myType); + myPojo.setNumber(new BigDecimal(number)); + myPojo.setOtherPojo(otherPojo); + return myPojo; + } + + private OtherPojo createOtherPojo(String currency) { + OtherPojo otherPojo = new OtherPojo(); + otherPojo.setCurrency(Currency.getInstance(currency)); + return otherPojo; + } + + public static class MyEntity { + private String value; + private List pojos; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public List getPojos() { + return pojos; + } + + public void setPojos(List pojos) { + this.pojos = pojos; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MyEntity myEntity = (MyEntity) o; + return Objects.equals(value, myEntity.value) && + Objects.equals(pojos, myEntity.pojos); + } + + @Override + public int hashCode() { + return Objects.hash(value, pojos); + } + } + + public static class MyPojo implements Serializable { + private String value; + private MyType type; + private BigDecimal number; + private OtherPojo otherPojo; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public MyType getType() { + return type; + } + + public void setType(MyType type) { + this.type = type; + } + + public BigDecimal getNumber() { + return number; + } + + public void setNumber(BigDecimal number) { + this.number = number; + } + + public OtherPojo getOtherPojo() { + return otherPojo; + } + + public void setOtherPojo(OtherPojo otherPojo) { + this.otherPojo = otherPojo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MyPojo myPojo = (MyPojo) o; + return Objects.equals(value, myPojo.value) && + type == myPojo.type && + Objects.equals(number, myPojo.number) && + Objects.equals(otherPojo, myPojo.otherPojo); + } + + @Override + public int hashCode() { + return Objects.hash(value, type, number, otherPojo); + } + } + + public enum MyType { + A, B, C + } + + public static class OtherPojo implements Serializable { + private Currency currency; + + public Currency getCurrency() { + return currency; + } + + public void setCurrency(Currency currency) { + this.currency = currency; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OtherPojo otherPojo = (OtherPojo) o; + return Objects.equals(currency, otherPojo.currency); + } + + @Override + public int hashCode() { + return Objects.hash(currency); + } + } + +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/internal/JsonTypeDescriptorTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/internal/JsonTypeDescriptorTest.java new file mode 100644 index 000000000..1bfb80eeb --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/internal/JsonTypeDescriptorTest.java @@ -0,0 +1,110 @@ +package com.vladmihalcea.hibernate.type.json.internal; + +import com.vladmihalcea.hibernate.type.model.BaseEntity; +import org.junit.Test; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +import static org.junit.Assert.assertTrue; + +public class JsonTypeDescriptorTest { + + /** + * If JSON serialization is used, + * the {@link JsonJavaTypeDescriptor#areEqual(Object, Object)} depends on the order of the elements. + *

+ * If the first collection contains the all element of another collection, + * then the two collection are equaled. + *

+ * If the JSON object of the `theFirst` form would be : + * { + * "formFields":[1, 2, 3] + * } + *

+ * And, the JSON object of the `theSecond` form would be: + * { + * "formFields":[3, 2, 1] + * } + *

+ * The two JSON objects should be equal. + */ + @Test + public void testSetsAreEqual() { + JsonJavaTypeDescriptor descriptor = new JsonJavaTypeDescriptor(); + + Form theFirst = createForm(1, 2, 3); + Form theSecond = createForm(3, 2, 1); + assertTrue(descriptor.areEqual(theFirst, theSecond)); + } + + private Form createForm(Integer... numbers) { + Form form = new Form(); + + Set formFields = new LinkedHashSet<>(); + + Arrays.asList(numbers).forEach(o -> { + FormField formField = new FormField(); + formField.setNumber(o); + formFields.add(formField); + }); + + form.setFormFields(formFields); + + return form; + } + + public static class FormField { + + private Integer number; + + public Integer getNumber() { + return number; + } + + public void setNumber(Integer number) { + this.number = number; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FormField formField = (FormField) o; + return Objects.equals(number, formField.number); + } + + @Override + public int hashCode() { + return Objects.hash(number); + } + } + + public static class Form extends BaseEntity { + + private Set formFields; + + public Set getFormFields() { + return formFields; + } + + public void setFormFields(Set formFields) { + this.formFields = formFields; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Form)) return false; + Form form = (Form) o; + return Objects.equals(formFields, form.formFields); + } + + @Override + public int hashCode() { + return Objects.hash(formFields); + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/polymorphic/PostgreSQLJsonPolymorphicListCustomTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/polymorphic/PostgreSQLJsonPolymorphicListCustomTypeTest.java new file mode 100644 index 000000000..c2fb04533 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/polymorphic/PostgreSQLJsonPolymorphicListCustomTypeTest.java @@ -0,0 +1,222 @@ +package com.vladmihalcea.hibernate.type.json.polymorphic; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.type.util.ObjectMapperWrapper; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonPolymorphicListCustomTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + ObjectMapper objectMapper = new ObjectMapperWrapper().getObjectMapper(); + properties.put("hibernate.type_contributors", + (TypeContributorList) () -> Collections.singletonList( + (typeContributions, serviceRegistry) -> + typeContributions.contributeType( + new JsonType( + objectMapper.activateDefaultTypingAsProperty( + objectMapper.getPolymorphicTypeValidator(), + ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE, + "type" + ), + ArrayList.class + ) { + @Override + public String getName() { + return "json-polymorphic-list"; + } + } + ) + ) + ); + } + + @Test + @Ignore("TODO: Unsupported YET!!!") + public void test() { + + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .addCoupon(new AmountDiscountCoupon("PPP") + .setAmount(new BigDecimal("4.99")) + ) + .addCoupon(new PercentageDiscountCoupon("Black Friday") + .setPercentage(BigDecimal.valueOf(0.02)) + ) + ); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + Map topics = book.getCoupons() + .stream() + .collect( + Collectors.toMap( + DiscountCoupon::getName, + Function.identity() + ) + ); + + assertEquals(2, topics.size()); + AmountDiscountCoupon amountDiscountCoupon = (AmountDiscountCoupon) + topics.get("PPP"); + assertEquals( + new BigDecimal("4.99"), + amountDiscountCoupon.getAmount() + ); + + PercentageDiscountCoupon percentageDiscountCoupon = (PercentageDiscountCoupon) + topics.get("Black Friday"); + assertEquals( + BigDecimal.valueOf(0.02), + percentageDiscountCoupon.getPercentage() + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + @Column(length = 15) + private String isbn; + + //@Type(type = "json-polymorphic-list") + @Column(columnDefinition = "jsonb") + private List coupons = new ArrayList<>(); + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public List getCoupons() { + return coupons; + } + + public Book setCoupons(List coupons) { + this.coupons = coupons; + return this; + } + + public Book addCoupon(DiscountCoupon topic) { + coupons.add(topic); + return this; + } + } + + public abstract static class DiscountCoupon implements Serializable { + + private String name; + + public DiscountCoupon() { + } + + public DiscountCoupon(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DiscountCoupon)) return false; + DiscountCoupon that = (DiscountCoupon) o; + return Objects.equals(getName(), that.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } + } + + public static class AmountDiscountCoupon extends DiscountCoupon { + + private BigDecimal amount; + + public AmountDiscountCoupon() { + } + + public AmountDiscountCoupon(String name) { + super(name); + } + + public BigDecimal getAmount() { + return amount; + } + + public AmountDiscountCoupon setAmount(BigDecimal amount) { + this.amount = amount; + return this; + } + } + + public static class PercentageDiscountCoupon extends DiscountCoupon { + + private BigDecimal percentage; + + public PercentageDiscountCoupon() { + } + + public PercentageDiscountCoupon(String name) { + super(name); + } + + public BigDecimal getPercentage() { + return percentage; + } + + public PercentageDiscountCoupon setPercentage(BigDecimal amount) { + this.percentage = amount; + return this; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/polymorphic/PostgreSQLJsonPolymorphicListJacksonTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/polymorphic/PostgreSQLJsonPolymorphicListJacksonTypeTest.java new file mode 100644 index 000000000..395d8df1e --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/json/polymorphic/PostgreSQLJsonPolymorphicListJacksonTypeTest.java @@ -0,0 +1,228 @@ +package com.vladmihalcea.hibernate.type.json.polymorphic; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.vladmihalcea.hibernate.type.json.JsonType; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonPolymorphicListJacksonTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .addCoupon(new AmountDiscountCoupon("PPP") + .setAmount(new BigDecimal("4.99")) + ) + .addCoupon(new PercentageDiscountCoupon("Black Friday") + .setPercentage(BigDecimal.valueOf(0.02)) + ) + ); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + Map topics = book.getCoupons() + .stream() + .collect( + Collectors.toMap( + DiscountCoupon::getName, + Function.identity() + ) + ); + + assertEquals(2, topics.size()); + AmountDiscountCoupon amountDiscountCoupon = (AmountDiscountCoupon) topics.get("PPP"); + assertEquals( + new BigDecimal("4.99"), + amountDiscountCoupon.getAmount() + ); + + PercentageDiscountCoupon percentageDiscountCoupon = (PercentageDiscountCoupon) topics.get("Black Friday"); + assertEquals( + BigDecimal.valueOf(0.02), + percentageDiscountCoupon.getPercentage() + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "jsonb") + private List coupons = new ArrayList<>(); + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public List getCoupons() { + return coupons; + } + + public Book setCoupons(List coupons) { + this.coupons = coupons; + return this; + } + + public Book addCoupon(DiscountCoupon topic) { + coupons.add(topic); + return this; + } + } + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" + ) + @JsonSubTypes({ + @JsonSubTypes.Type( + name = "discount.coupon.amount", + value = AmountDiscountCoupon.class + ), + @JsonSubTypes.Type( + name = "discount.coupon.percentage", + value = PercentageDiscountCoupon.class + ), + }) + public abstract static class DiscountCoupon implements Serializable { + + private String name; + + public DiscountCoupon() { + } + + public DiscountCoupon(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) + public abstract String getType(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DiscountCoupon)) return false; + DiscountCoupon that = (DiscountCoupon) o; + return Objects.equals(getName(), that.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } + } + + public static class AmountDiscountCoupon extends DiscountCoupon { + + public static final String DISCRIMINATOR = "discount.coupon.amount"; + + private BigDecimal amount; + + public AmountDiscountCoupon() { + } + + public AmountDiscountCoupon(String name) { + super(name); + } + + public BigDecimal getAmount() { + return amount; + } + + public AmountDiscountCoupon setAmount(BigDecimal amount) { + this.amount = amount; + return this; + } + + @Override + public String getType() { + return DISCRIMINATOR; + } + } + + public static class PercentageDiscountCoupon extends DiscountCoupon { + + public static final String DISCRIMINATOR = "discount.coupon.percentage"; + + private BigDecimal percentage; + + public PercentageDiscountCoupon() { + } + + public PercentageDiscountCoupon(String name) { + super(name); + } + + public BigDecimal getPercentage() { + return percentage; + } + + public PercentageDiscountCoupon setPercentage(BigDecimal amount) { + this.percentage = amount; + return this; + } + + @Override + public String getType() { + return DISCRIMINATOR; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/BaseEntity.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/BaseEntity.java new file mode 100644 index 000000000..1480c1838 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/BaseEntity.java @@ -0,0 +1,30 @@ +package com.vladmihalcea.hibernate.type.model; + +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Version; + +/** + * @author Vlad Mihalcea + */ +@MappedSuperclass +public class BaseEntity { + + @Id + private Long id; + + @Version + private Integer version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Integer getVersion() { + return version; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/Event.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/Event.java new file mode 100644 index 000000000..436c83301 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/Event.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hibernate.type.model; + +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; +import jakarta.persistence.*; +import org.hibernate.annotations.Type; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Event") +@Table(name = "event") +public class Event extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + @Basic(fetch = FetchType.LAZY) + private Location location; + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/Location.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/Location.java new file mode 100644 index 000000000..33c6ea18f --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/Location.java @@ -0,0 +1,52 @@ +package com.vladmihalcea.hibernate.type.model; + +import java.io.Serializable; +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +public class Location implements Serializable { + + private String country; + + private String city; + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + @Override + public String toString() { + return "Location{" + + "country='" + country + '\'' + + ", city='" + city + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Location location = (Location) o; + return Objects.equals(country, location.country) && + Objects.equals(city, location.city); + } + + @Override + public int hashCode() { + return Objects.hash(country, city); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/Participant.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/Participant.java new file mode 100644 index 000000000..d00015a8a --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/Participant.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hibernate.type.model; + +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; +import jakarta.persistence.*; +import org.hibernate.annotations.Type; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Participant") +@Table(name = "participant") +public class Participant extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + @Basic(fetch = FetchType.LAZY) + private Ticket ticket; + + @ManyToOne(fetch = FetchType.LAZY) + private Event event; + + public Ticket getTicket() { + return ticket; + } + + public void setTicket(Ticket ticket) { + this.ticket = ticket; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/Ticket.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/Ticket.java new file mode 100644 index 000000000..921717e0b --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/model/Ticket.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hibernate.type.model; + +import java.io.Serializable; +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +public class Ticket implements Serializable { + + private String registrationCode; + + private double price; + + public String getRegistrationCode() { + return registrationCode; + } + + public void setRegistrationCode(String registrationCode) { + this.registrationCode = registrationCode; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Ticket ticket = (Ticket) o; + return Double.compare(ticket.price, price) == 0 && + Objects.equals(registrationCode, ticket.registrationCode); + } + + @Override + public int hashCode() { + return Objects.hash(registrationCode, price); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/PostgreSQLRangeTypeDSTTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/PostgreSQLRangeTypeDSTTest.java new file mode 100644 index 000000000..3f128fd45 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/PostgreSQLRangeTypeDSTTest.java @@ -0,0 +1,83 @@ +package com.vladmihalcea.hibernate.type.range; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static com.vladmihalcea.hibernate.type.range.Range.zonedDateTimeRange; +import static org.junit.Assert.assertEquals; + +/** + * @author Edgar Asatryan + */ +public class PostgreSQLRangeTypeDSTTest extends AbstractPostgreSQLIntegrationTest { + + // updated to cross DST boundary (31/9/2018) + private final Range tsTz = zonedDateTimeRange("[\"2018-05-03T10:15:30+12:00\",\"2018-12-03T10:15:30+12:00\"]"); + + @Override + protected Class[] entities() { + return new Class[]{ + Restriction.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Restriction restriction = new Restriction(); + restriction.setId(1L); + restriction.setRangeZonedDateTime(tsTz); + entityManager.persist(restriction); + + return restriction; + }); + + doInJPA(entityManager -> { + Restriction ar = entityManager.find(Restriction.class, 1L); + + ZoneId zone = ar.getRangeZonedDateTime().lower().getZone(); + + ZonedDateTime lower = tsTz.lower().withZoneSameInstant(zone); + + assertEquals(lower, ar.getRangeZonedDateTime().lower()); + assertEquals(LocalDateTime.parse("2018-12-03T10:15:30").atZone(ZoneId.systemDefault()).getOffset(), ar.getRangeZonedDateTime().upper().getOffset()); + }); + } + + @Entity(name = "AgeRestriction") + @Table(name = "age_restriction") + public static class Restriction { + + @Id + private Long id; + + @Type(PostgreSQLRangeType.class) + @Column(name = "r_tstzrange", columnDefinition = "tstzrange") + private Range rangeZonedDateTime; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Range getRangeZonedDateTime() { + return rangeZonedDateTime; + } + + public void setRangeZonedDateTime(Range rangeZonedDateTime) { + this.rangeZonedDateTime = rangeZonedDateTime; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/PostgreSQLRangeTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/PostgreSQLRangeTypeTest.java new file mode 100644 index 000000000..11f46f9de --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/PostgreSQLRangeTypeTest.java @@ -0,0 +1,235 @@ +package com.vladmihalcea.hibernate.type.range; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static com.vladmihalcea.hibernate.type.range.Range.infinite; +import static com.vladmihalcea.hibernate.type.range.Range.zonedDateTimeRange; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Edgar Asatryan + */ +public class PostgreSQLRangeTypeTest extends AbstractPostgreSQLIntegrationTest { + + private final Range numeric = Range.bigDecimalRange("[0.5,0.89]"); + + private final Range int8Range = Range.longRange("[0,18)"); + + private final Range int4Range = infinite(Integer.class); + + private final Range int4RangeEmpty = Range.integerRange("[123,123)"); + + private final Range int4RangeInfinity = Range.integerRange("[123,infinity)"); + + private final Range localDateTimeRange = Range.localDateTimeRange("[2014-04-28 16:00:49,2015-04-28 16:00:49]"); + + private final Range tsTz = zonedDateTimeRange("[\"2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00\"]"); + + private final Range infinityTsTz = zonedDateTimeRange("[\"2007-12-03T10:15:30+01:00\",infinity)"); + + private final Range dateRange = Range.localDateRange("[1992-01-13,1995-01-13)"); + + @Override + protected Class[] entities() { + return new Class[]{ + Restriction.class + }; + } + + @Test + public void test() { + Restriction _restriction = doInJPA(entityManager -> { + entityManager.persist(new Restriction()); + + Restriction restriction = new Restriction(); + restriction.setRangeInt(int4Range); + restriction.setRangeIntEmpty(int4RangeEmpty); + restriction.setRangeIntInfinity(int4RangeInfinity); + restriction.setRangeLong(int8Range); + restriction.setRangeBigDecimal(numeric); + restriction.setRangeLocalDateTime(localDateTimeRange); + restriction.setRangeZonedDateTime(tsTz); + restriction.setRangeZonedDateTimeInfinity(infinityTsTz); + restriction.setRangeLocalDate(dateRange); + entityManager.persist(restriction); + + return restriction; + }); + + doInJPA(entityManager -> { + Restriction restriction = entityManager.find(Restriction.class, _restriction.getId()); + + assertEquals(int4Range, restriction.getRangeInt()); + assertEquals(Range.emptyRange(Integer.class), restriction.getRangeIntEmpty()); + assertEquals(int4RangeInfinity, restriction.getRangeIntInfinity()); + assertEquals(int8Range, restriction.getRangeLong()); + assertEquals(numeric, restriction.getRangeBigDecimal()); + assertEquals(localDateTimeRange, restriction.getLocalDateTimeRange()); + assertEquals(dateRange, restriction.getRangeLocalDate()); + + ZoneId zone = restriction.getRangeZonedDateTime().lower().getZone(); + + ZonedDateTime lower = tsTz.lower().withZoneSameInstant(zone); + ZonedDateTime upper = tsTz.upper().withZoneSameInstant(zone); + assertEquals(restriction.getRangeZonedDateTime(), Range.closed(lower, upper)); + + lower = infinityTsTz.lower().withZoneSameInstant(zone); + assertEquals(restriction.getRangeZonedDateTimeInfinity(), Range.closedInfinite(lower)); + }); + } + + @Test + public void testNullRange() { + Restriction _restriction = doInJPA(entityManager -> { + Restriction restriction = new Restriction(); + entityManager.persist(restriction); + + return restriction; + }); + + doInJPA(entityManager -> { + Restriction restriction = entityManager.find(Restriction.class, _restriction.getId()); + + assertNull(restriction.getRangeInt()); + assertNull(restriction.getRangeIntEmpty()); + assertNull(restriction.getRangeIntInfinity()); + assertNull(restriction.getRangeLong()); + assertNull(restriction.getRangeBigDecimal()); + assertNull(restriction.getLocalDateTimeRange()); + assertNull(restriction.getRangeLocalDate()); + assertNull(restriction.getRangeZonedDateTime()); + assertNull(restriction.getRangeZonedDateTimeInfinity()); + }); + } + + @Entity(name = "AgeRestriction") + @Table(name = "age_restriction") + public static class Restriction { + + @Id + @GeneratedValue + private Long id; + + @Type(PostgreSQLRangeType.class) + @Column(name = "r_int", columnDefinition = "int4Range") + private Range rangeInt; + + @Type(PostgreSQLRangeType.class) + @Column(name = "r_int_empty", columnDefinition = "int4Range") + private Range rangeIntEmpty; + + @Type(PostgreSQLRangeType.class) + @Column(name = "r_int_infinity", columnDefinition = "int4Range") + private Range rangeIntInfinity; + + @Type(PostgreSQLRangeType.class) + @Column(name = "r_long", columnDefinition = "int8range") + private Range rangeLong; + + @Type(PostgreSQLRangeType.class) + @Column(name = "r_numeric", columnDefinition = "numrange") + private Range rangeBigDecimal; + + @Type(PostgreSQLRangeType.class) + @Column(name = "r_ts", columnDefinition = "tsrange") + private Range rangeLocalDateTime; + + @Type(PostgreSQLRangeType.class) + @Column(name = "r_ts_tz", columnDefinition = "tstzrange") + private Range rangeZonedDateTime; + + @Type(PostgreSQLRangeType.class) + @Column(name = "r_ts_tz_infinity", columnDefinition = "tstzrange") + private Range rangeZonedDateTimeInfinity; + + @Type(PostgreSQLRangeType.class) + @Column(name = "r_date", columnDefinition = "daterange") + private Range rangeLocalDate; + + public Long getId() { + return id; + } + + public Range getRangeInt() { + return rangeInt; + } + + public void setRangeInt(Range rangeInt) { + this.rangeInt = rangeInt; + } + + public Range getRangeIntEmpty() { + return rangeIntEmpty; + } + + public void setRangeIntEmpty(Range rangeIntEmpty) { + this.rangeIntEmpty = rangeIntEmpty; + } + + public Range getRangeIntInfinity() { + return rangeIntInfinity; + } + + public void setRangeIntInfinity(Range rangeIntInfinity) { + this.rangeIntInfinity = rangeIntInfinity; + } + + public Range getRangeLong() { + return rangeLong; + } + + public void setRangeLong(Range rangeLong) { + this.rangeLong = rangeLong; + } + + public Range getRangeBigDecimal() { + return rangeBigDecimal; + } + + public void setRangeBigDecimal(Range rangeBigDecimal) { + this.rangeBigDecimal = rangeBigDecimal; + } + + public Range getLocalDateTimeRange() { + return rangeLocalDateTime; + } + + public void setRangeLocalDateTime(Range rangeLocalDateTime) { + this.rangeLocalDateTime = rangeLocalDateTime; + } + + public Range getRangeZonedDateTime() { + return rangeZonedDateTime; + } + + public void setRangeZonedDateTime(Range rangeZonedDateTime) { + this.rangeZonedDateTime = rangeZonedDateTime; + } + + public Range getRangeZonedDateTimeInfinity() { + return rangeZonedDateTimeInfinity; + } + + public void setRangeZonedDateTimeInfinity(Range rangeZonedDateTimeInfinity) { + this.rangeZonedDateTimeInfinity = rangeZonedDateTimeInfinity; + } + + public Range getRangeLocalDate() { + return rangeLocalDate; + } + + public void setRangeLocalDate(Range rangeLocalDate) { + this.rangeLocalDate = rangeLocalDate; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/RangeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/RangeTest.java new file mode 100644 index 000000000..a9441b687 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/RangeTest.java @@ -0,0 +1,77 @@ +package com.vladmihalcea.hibernate.type.range; + +import org.junit.Test; + +import static com.vladmihalcea.hibernate.type.range.Range.integerRange; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +/** + * @author Edgar Asatryan + */ +public class RangeTest { + + @Test + public void ofStringTest() { + assertThat(integerRange("[1,3]").lower(), is(1)); + assertThat(integerRange("[1,3]").upper(), is(3)); + assertThat(integerRange("[1,3]").isUpperBoundClosed(), is(true)); + assertThat(integerRange("[1,3]").isLowerBoundClosed(), is(true)); + + assertThat(integerRange("[,3]").lower(), is(nullValue())); + assertThat(integerRange("[,3]").upper(), is(3)); + assertThat(integerRange("[,3]").hasLowerBound(), is(false)); + assertThat(integerRange("[,3]").hasUpperBound(), is(true)); + assertThat(integerRange("[,3]").isUpperBoundClosed(), is(true)); + assertThat(integerRange("[,3]").isLowerBoundClosed(), is(false)); + + assertThat(integerRange("[,]").lower(), is(nullValue())); + assertThat(integerRange("[,]").upper(), is(nullValue())); + assertThat(integerRange("[,]").hasLowerBound(), is(false)); + assertThat(integerRange("[,]").hasUpperBound(), is(false)); + assertThat(integerRange("[,]").isUpperBoundClosed(), is(false)); + assertThat(integerRange("[,]").isLowerBoundClosed(), is(false)); + + assertThat(integerRange("(-5,5]").isUpperBoundClosed(), is(true)); + assertThat(integerRange("(-5,5]").isLowerBoundClosed(), is(false)); + } + + @Test + public void containsRange() { + assertThat(integerRange("[-5,5]").contains(integerRange("[-4,4]")), is(true)); + assertThat(integerRange("[-5,5]").contains(integerRange("[-5,5]")), is(true)); + assertThat(integerRange("(-5,5]").contains(integerRange("[-4,4]")), is(true)); + assertThat(integerRange("(-5,5]").contains(integerRange("(-4,4]")), is(true)); + + assertThat(integerRange("(,)").contains(integerRange("(,)")), is(true)); + assertThat(integerRange("(5,)").contains(integerRange("(6,)")), is(true)); + assertThat(integerRange("(,5)").contains(integerRange("(,4)")), is(true)); + assertThat(integerRange("(,)").contains(integerRange("(6,)")), is(true)); + assertThat(integerRange("(,)").contains(integerRange("(,6)")), is(true)); + } + + @Test + public void localDateTimeTest() { + assertNotNull(Range.localDateTimeRange("[2019-03-27 16:33:10.1,)")); + assertNotNull(Range.localDateTimeRange("[2019-03-27 16:33:10.12,)")); + assertNotNull(Range.localDateTimeRange("[2019-03-27 16:33:10.123,)")); + assertNotNull(Range.localDateTimeRange("[2019-03-27 16:33:10.1234,)")); + assertNotNull(Range.localDateTimeRange("[2019-03-27 16:33:10.12345,)")); + assertNotNull(Range.localDateTimeRange("[2019-03-27 16:33:10.123456,)")); + assertNotNull(Range.localDateTimeRange("[2019-03-27 16:33:10.123456,infinity)")); + } + + @Test + public void zonedDateTimeTest() { + assertNotNull(Range.zonedDateTimeRange("[2019-03-27 16:33:10.1-06,)")); + assertNotNull(Range.zonedDateTimeRange("[2019-03-27 16:33:10.12-06,)")); + assertNotNull(Range.zonedDateTimeRange("[2019-03-27 16:33:10.123-06,)")); + assertNotNull(Range.zonedDateTimeRange("[2019-03-27 16:33:10.1234-06,)")); + assertNotNull(Range.zonedDateTimeRange("[2019-03-27 16:33:10.12345-06,)")); + assertNotNull(Range.zonedDateTimeRange("[2019-03-27 16:33:10.123456-06,)")); + assertNotNull(Range.zonedDateTimeRange("[2019-03-27 16:33:10.123456+05:30,)")); + assertNotNull(Range.zonedDateTimeRange("[2019-03-27 16:33:10.123456-06,infinity)")); + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/ZonedDateTimeMilliSecondTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/ZonedDateTimeMilliSecondTest.java new file mode 100644 index 000000000..1726891bd --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/ZonedDateTimeMilliSecondTest.java @@ -0,0 +1,107 @@ +package com.vladmihalcea.hibernate.type.range; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static com.vladmihalcea.hibernate.type.range.Range.zonedDateTimeRange; +import static org.junit.Assert.assertEquals; + +/** + * @author Arun Mohandas + */ +public class ZonedDateTimeMilliSecondTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Restriction.class + }; + } + + @Test + public void test() { + validateTest( + zonedDateTimeRange( + "[" + + "\"2018-05-03T10:15:30.127110+12:00\"," + + "\"2018-12-03T10:15:30.127111+12:00\"" + + "]" + ) + ); + + validateTest( + zonedDateTimeRange( + "[" + + "\"2018-05-03T10:15:30.127100+12:00\"," + + "\"2018-12-03T10:15:30.127111+12:00\"" + + "]" + ) + ); + + validateTest( + zonedDateTimeRange( + "[" + + "\"2018-05-03T10:15:30.127000+12:00\"," + + "\"2018-12-03T10:15:30.127111+12:00\"" + + "]" + ) + ); + } + + private void validateTest(Range tsTz) { + Restriction _restriction = doInJPA(entityManager -> { + Restriction restriction = new Restriction(); + restriction.setRangeZonedDateTime(tsTz); + entityManager.persist(restriction); + + return restriction; + }); + + doInJPA(entityManager -> { + Restriction restriction = entityManager.find(Restriction.class, _restriction.getId()); + + ZoneId zone = restriction.getRangeZonedDateTime().lower().getZone(); + + ZonedDateTime lower = tsTz.lower().withZoneSameInstant(zone); + + assertEquals(lower, restriction.getRangeZonedDateTime().lower()); + assertEquals(LocalDateTime.parse("2018-12-03T10:15:30").atZone(ZoneId.systemDefault()).getOffset(), + restriction.getRangeZonedDateTime().upper().getOffset()); + }); + } + + @Entity(name = "AgeRestriction") + @Table(name = "age_restriction") + public static class Restriction { + + @Id + @GeneratedValue + private Long id; + + @Type(PostgreSQLRangeType.class) + @Column(name = "r_tstzrange", columnDefinition = "tstzrange") + private Range rangeZonedDateTime; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Range getRangeZonedDateTime() { + return rangeZonedDateTime; + } + + public void setRangeZonedDateTime(Range rangeZonedDateTime) { + this.rangeZonedDateTime = rangeZonedDateTime; + } + } +} \ No newline at end of file diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/guava/PostgreSQLGuavaRangeTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/guava/PostgreSQLGuavaRangeTypeTest.java new file mode 100644 index 000000000..cd96e86b2 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/range/guava/PostgreSQLGuavaRangeTypeTest.java @@ -0,0 +1,244 @@ +package com.vladmihalcea.hibernate.type.range.guava; + +import com.google.common.collect.Range; +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.time.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Edgar Asatryan + * @author Jan-Willem Gmelig Meyling + */ +public class PostgreSQLGuavaRangeTypeTest extends AbstractPostgreSQLIntegrationTest { + + private static final ZoneOffset DEFAULT_OFFSET = OffsetDateTime.now().getOffset(); + + private final Range numeric = Range.closedOpen(new BigDecimal("0.5"), new BigDecimal("0.89")); + + private final Range int8Range = Range.closedOpen(0L, 18L); + + private final Range int4Range = Range.closedOpen(0, 18); + + private final Range localDateTimeRange = Range.closed( + LocalDateTime.of(2014, Month.APRIL, 28, 16, 0, 49), + LocalDateTime.of(2015, Month.APRIL, 28, 16, 0, 49)); + + private final Range tsTz = Range.closed( + OffsetDateTime.of(LocalDateTime.of(2007, Month.DECEMBER, 3, 10, 15, 30), ZoneOffset.ofHours(1)).toZonedDateTime(), + OffsetDateTime.of(LocalDateTime.of(2008, Month.DECEMBER, 3, 10, 15, 30), ZoneOffset.ofHours(1)).toZonedDateTime()); + + private final Range tsTzO = Range.closed( + OffsetDateTime.of(LocalDateTime.of(2007, Month.MAY, 3, 10, 15, 30), DEFAULT_OFFSET), + OffsetDateTime.of(LocalDateTime.of(2008, Month.MAY, 3, 10, 15, 30), DEFAULT_OFFSET) + ); + + private final Range dateRange = Range.closedOpen(LocalDate.of(1992, Month.JANUARY, 13), LocalDate.of(1995, Month.JANUARY, 13)); + + @Override + protected Class[] entities() { + return new Class[]{ + Restriction.class + }; + } + + @Test + public void test() { + Restriction ageRestrictionInt = doInJPA(entityManager -> { + entityManager.persist(new Restriction()); + + Restriction restriction = new Restriction(); + restriction.setRangeInt(int4Range); + restriction.setRangeLong(int8Range); + restriction.setRangeBigDecimal(numeric); + restriction.setRangeLocalDateTime(localDateTimeRange); + restriction.setRangeZonedDateTime(tsTz); + restriction.setLocalDateRange(dateRange); + restriction.setOffsetZonedDateTime(tsTzO); + entityManager.persist(restriction); + + return restriction; + }); + + doInJPA(entityManager -> { + Restriction ar = entityManager.find(Restriction.class, ageRestrictionInt.getId()); + + assertEquals(int4Range, ar.getRangeInt()); + assertEquals(int8Range, ar.getRangeLong()); + assertEquals(numeric, ar.getRangeBigDecimal()); + assertEquals(localDateTimeRange, ar.getLocalDateTimeRange()); + //assertEquals(tsTzO, ar.getOffsetZonedDateTime()); + assertEquals(dateRange, ar.getLocalDateRange()); + + ZoneId zone = ar.getRangeZonedDateTime().lowerEndpoint().getZone(); + + ZonedDateTime lower = tsTz.lowerEndpoint().withZoneSameInstant(zone); + ZonedDateTime upper = tsTz.upperEndpoint().withZoneSameInstant(zone); + + assertEquals(ar.getRangeZonedDateTime(), Range.closed(lower, upper)); + }); + } + + @Test + public void testNullRange() { + Restriction ageRestrictionInt = doInJPA(entityManager -> { + Restriction restriction = new Restriction(); + entityManager.persist(restriction); + + return restriction; + }); + + doInJPA(entityManager -> { + Restriction ar = entityManager.find(Restriction.class, ageRestrictionInt.getId()); + + assertNull(ar.getRangeInt()); + assertNull(ar.getRangeLong()); + assertNull(ar.getRangeBigDecimal()); + assertNull(ar.getLocalDateTimeRange()); + assertNull(ar.getLocalDateRange()); + assertNull(ar.getRangeZonedDateTime()); + }); + } + + @Test + public void testUnboundedRangeIsRejected() { + Restriction ageRestrictionInt = doInJPA(entityManager -> { + Restriction restriction = new Restriction(); + restriction.setRangeInt(Range.all()); + entityManager.persist(restriction); + + return restriction; + }); + + + doInJPA(entityManager -> { + Restriction ar = entityManager.find(Restriction.class, ageRestrictionInt.getId()); + assertEquals(ar.getRangeInt(), Range.all()); + }); + } + + @Test + public void testUnboundedRangeStringIsRejected() { + PostgreSQLGuavaRangeType instance = PostgreSQLGuavaRangeType.INSTANCE; + assertEquals(Range.all(), instance.integerRange("(,)")); + } + + @Test + public void testSingleBoundedRanges() { + PostgreSQLGuavaRangeType instance = PostgreSQLGuavaRangeType.INSTANCE; + + assertEquals("(,)", instance.asString(Range.all())); + assertEquals("(1,)", instance.asString(Range.greaterThan(1))); + assertEquals("[2,)", instance.asString(Range.atLeast(2))); + assertEquals("(,3)", instance.asString(Range.lessThan(3))); + assertEquals("(,4]", instance.asString(Range.atMost(4))); + + assertEquals(Range.greaterThan(5), instance.integerRange("(5,)")); + assertEquals(Range.atLeast(6), instance.integerRange("[6,)")); + assertEquals(Range.lessThan(7), instance.integerRange("(,7)")); + assertEquals(Range.atMost(8), instance.integerRange("(,8]")); + } + + @Entity(name = "AgeRestriction") + @Table(name = "age_restriction") + public static class Restriction { + + @Id + @GeneratedValue + private Long id; + + @Type(PostgreSQLGuavaRangeType.class) + @Column(name = "r_int", columnDefinition = "int4Range") + private Range rangeInt; + + @Type(PostgreSQLGuavaRangeType.class) + @Column(name = "r_long", columnDefinition = "int8range") + private Range rangeLong; + + @Type(PostgreSQLGuavaRangeType.class) + @Column(name = "r_numeric", columnDefinition = "numrange") + private Range rangeBigDecimal; + + @Type(PostgreSQLGuavaRangeType.class) + @Column(name = "r_tsrange", columnDefinition = "tsrange") + private Range rangeLocalDateTime; + + @Type(PostgreSQLGuavaRangeType.class) + @Column(name = "r_tstzrange", columnDefinition = "tstzrange") + private Range rangeZonedDateTime; + + @Type(PostgreSQLGuavaRangeType.class) + @Column(name = "r_otstzrange", columnDefinition = "tstzrange") + private Range offsetZonedDateTime; + + @Type(PostgreSQLGuavaRangeType.class) + @Column(name = "r_daterange", columnDefinition = "daterange") + private Range localDateRange; + + public Long getId() { + return id; + } + + public Range getRangeLong() { + return rangeLong; + } + + public void setRangeLong(Range rangeLong) { + this.rangeLong = rangeLong; + } + + public Range getRangeInt() { + return rangeInt; + } + + public void setRangeInt(Range rangeInt) { + this.rangeInt = rangeInt; + } + + public Range getRangeBigDecimal() { + return rangeBigDecimal; + } + + public void setRangeBigDecimal(Range rangeBigDecimal) { + this.rangeBigDecimal = rangeBigDecimal; + } + + public Range getLocalDateTimeRange() { + return rangeLocalDateTime; + } + + public void setRangeLocalDateTime(Range rangeLocalDateTime) { + this.rangeLocalDateTime = rangeLocalDateTime; + } + + public Range getRangeZonedDateTime() { + return rangeZonedDateTime; + } + + public void setRangeZonedDateTime(Range rangeZonedDateTime) { + this.rangeZonedDateTime = rangeZonedDateTime; + } + + public Range getLocalDateRange() { + return localDateRange; + } + + public void setLocalDateRange(Range localDateRange) { + this.localDateRange = localDateRange; + } + + public Range getOffsetZonedDateTime() { + return offsetZonedDateTime; + } + + public void setOffsetZonedDateTime(Range offsetZonedDateTime) { + this.offsetZonedDateTime = offsetZonedDateTime; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/search/PostgreSQLTSVectorTypeTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/search/PostgreSQLTSVectorTypeTest.java new file mode 100644 index 000000000..e1367f3aa --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/search/PostgreSQLTSVectorTypeTest.java @@ -0,0 +1,94 @@ +package com.vladmihalcea.hibernate.type.search; + +import com.vladmihalcea.hibernate.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + * @author Philip Riecks + */ +public class PostgreSQLTSVectorTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setId(1L); + book.setIsbn("978-9730228236"); + book.setFts( + "This book is a journey into Java data access performance tuning. From connection management, to batch" + + " updates, fetch sizes and concurrency control mechanisms, it unravels the inner workings of" + + " the most common Java data access frameworks." + ); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, 1L); + + assertTrue(book.getFts().contains("Java")); + assertTrue(book.getFts().contains("concurrency")); + assertTrue(book.getFts().contains("book")); + }); + } + + @Override + protected List additionalTypes() { + return List.of(PostgreSQLTSVectorType.INSTANCE); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + private Long id; + + @NaturalId + private String isbn; + + @Column(columnDefinition = "tsvector") + private String fts; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getFts() { + return fts; + } + + public void setFts(String fts) { + this.fts = fts; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/ConfigurationTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/ConfigurationTest.java new file mode 100644 index 000000000..e0a343211 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/ConfigurationTest.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hibernate.type.util; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ConfigurationTest { + + @Test + public void testHibernateProperties() { + assertNull(Configuration.INSTANCE.getProperties().getProperty("hibernate.types.nothing")); + assertEquals("def", Configuration.INSTANCE.getProperties().getProperty("hibernate.types.abc")); + } + + @Test + public void testHibernateTypesOverrideProperties() { + assertEquals("ghi", Configuration.INSTANCE.getProperties().getProperty("hibernate.types.def")); + } + + @Test + public void testApplicationProperties() { + assertNull(Configuration.INSTANCE.getProperties().getProperty("hibernate.types.app.props.no")); + assertEquals("true", Configuration.INSTANCE.getProperties().getProperty("hibernate.types.app.props")); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/ObjectMapperJsonSerializerTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/ObjectMapperJsonSerializerTest.java new file mode 100644 index 000000000..8f6610b6d --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/ObjectMapperJsonSerializerTest.java @@ -0,0 +1,301 @@ +package com.vladmihalcea.hibernate.type.util; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.Lists; +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import jakarta.persistence.Column; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; + +public class ObjectMapperJsonSerializerTest { + + private ObjectMapperWrapper mapper = new ObjectMapperWrapper(); + + private ObjectMapperJsonSerializer serializer = new ObjectMapperJsonSerializer(mapper); + + @Test + public void should_clone_serializable_object() { + Object original = new SerializableObject("value"); + Object cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_non_serializable_object() { + Object original = new NonSerializableObject("value"); + Object cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_collection_of_serializable_object() { + List original = new ArrayList<>(); + original.add(new SerializableObject("value")); + List cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_collection_of_non_serializable_object() { + List original = new ArrayList<>(); + original.add(new NonSerializableObject("value")); + List cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_empty_collection() { + List original = new ArrayList<>(); + Object cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_map_of_non_serializable_key() { + Map original = new HashMap<>(); + original.put(new NonSerializableObject("key"), "value"); + Object cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_map_of_non_serializable_value() { + Map original = new HashMap<>(); + original.put("key", new NonSerializableObject("value")); + Object cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_map_of_serializable_key_and_value() { + Map original = new HashMap<>(); + original.put("key", new SerializableObject("value")); + Object cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_map_with_null_value() { + Map original = new HashMap<>(); + original.put("null", null); + Object cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_map_of_non_serializable_value_with_null_value() { + Map original = new LinkedHashMap<>(); + original.put("null", null); + original.put("key", new NonSerializableObject("value")); + Object cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_map_of_serializable_key_and_value_with_null() { + Map original = new LinkedHashMap<>(); + original.put("null", null); + original.put("key", new SerializableObject("value")); + Object cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_serializable_complex_object_with_serializable_nested_object() { + Map> map = new LinkedHashMap<>(); + map.put("key1", Lists.newArrayList(new SerializableObject("name1"))); + map.put("key2", Lists.newArrayList( + new SerializableObject("name2"), + new SerializableObject("name3") + )); + Object original = new SerializableComplexObject(map); + Object cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_serializable_complex_object_with_non_serializable_nested_object() { + Map> map = new LinkedHashMap<>(); + map.put("key1", Lists.newArrayList(new NonSerializableObject("name1"))); + map.put("key2", Lists.newArrayList( + new NonSerializableObject("name2"), + new NonSerializableObject("name3") + )); + Object original = new SerializableComplexObjectWithNonSerializableNestedObject(map); + Object cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + @Test + public void should_clone_jsonnode() { + Object original = mapper.getObjectMapper().createArrayNode() + .add(BigDecimal.ONE) + .add(1.0) + .add("string"); + Object cloned = serializer.clone(original); + assertEquals(original, cloned); + assertNotSame(original, cloned); + } + + private static class SerializableObject implements Serializable { + private final String value; + + private SerializableObject(@JsonProperty("value") String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SerializableObject that = (SerializableObject) o; + + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return value; + } + } + + private static class NonSerializableObject { + private final String value; + + private NonSerializableObject(@JsonProperty("value") String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + NonSerializableObject that = (NonSerializableObject) o; + + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return value; + } + } + + private static class SerializableComplexObject implements Serializable { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private final Map> value; + + private SerializableComplexObject(@JsonProperty("value") Map> value) { + this.value = value; + } + + public Map> getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SerializableComplexObject that = (SerializableComplexObject) o; + + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return value.toString(); + } + } + + private static class SerializableComplexObjectWithNonSerializableNestedObject implements Serializable { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private final Map> value; + + private SerializableComplexObjectWithNonSerializableNestedObject(@JsonProperty("value") Map> value) { + this.value = value; + } + + public Map> getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SerializableComplexObjectWithNonSerializableNestedObject that = (SerializableComplexObjectWithNonSerializableNestedObject) o; + + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return value.toString(); + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/dto/DTOProjectionImportRelativePathTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/dto/DTOProjectionImportRelativePathTest.java new file mode 100644 index 000000000..d32eebd61 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/dto/DTOProjectionImportRelativePathTest.java @@ -0,0 +1,149 @@ +package com.vladmihalcea.hibernate.type.util.dto; + +import com.vladmihalcea.hibernate.type.util.ClassImportIntegrator; +import com.vladmihalcea.hibernate.util.AbstractTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.integrator.spi.Integrator; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DTOProjectionImportRelativePathTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + protected Integrator integrator() { + return new ClassImportIntegrator(Arrays.asList(PostDTO.class)) + .excludePath("com.vladmihalcea.hibernate.type"); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + post.setCreatedBy("Vlad Mihalcea"); + post.setCreatedOn(Timestamp.from( + LocalDateTime.of(2020, 11, 2, 12, 0, 0).toInstant(ZoneOffset.UTC) + )); + post.setUpdatedBy("Vlad Mihalcea"); + post.setUpdatedOn(Timestamp.from( + LocalDateTime.now().toInstant(ZoneOffset.UTC) + )); + + entityManager.persist(post); + }); + } + + @Test + public void testConstructorExpression() { + doInJPA(entityManager -> { + List postDTOs = entityManager.createQuery( + "select new util.dto.PostDTO(" + + " p.id, " + + " p.title " + + ") " + + "from Post p " + + "where p.createdOn > :fromTimestamp", PostDTO.class) + .setParameter( + "fromTimestamp", + Timestamp.from( + LocalDate.of(2020, 1, 1) + .atStartOfDay() + .toInstant(ZoneOffset.UTC) + ) + ) + .getResultList(); + + assertEquals(1, postDTOs.size()); + }); + } + + @Entity(name = "Post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private Timestamp createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private Timestamp updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Timestamp getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Timestamp createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Timestamp getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(Timestamp updatedOn) { + this.updatedOn = updatedOn; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/dto/DTOProjectionImportTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/dto/DTOProjectionImportTest.java new file mode 100644 index 000000000..0f233038b --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/dto/DTOProjectionImportTest.java @@ -0,0 +1,148 @@ +package com.vladmihalcea.hibernate.type.util.dto; + +import com.vladmihalcea.hibernate.type.util.ClassImportIntegrator; +import com.vladmihalcea.hibernate.util.AbstractTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.integrator.spi.Integrator; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DTOProjectionImportTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + protected Integrator integrator() { + return new ClassImportIntegrator(Arrays.asList(PostDTO.class)); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + post.setCreatedBy("Vlad Mihalcea"); + post.setCreatedOn(Timestamp.from( + LocalDateTime.of(2020, 11, 2, 12, 0, 0).toInstant(ZoneOffset.UTC) + )); + post.setUpdatedBy("Vlad Mihalcea"); + post.setUpdatedOn(Timestamp.from( + LocalDateTime.now().toInstant(ZoneOffset.UTC) + )); + + entityManager.persist(post); + }); + } + + @Test + public void testConstructorExpression() { + doInJPA(entityManager -> { + List postDTOs = entityManager.createQuery( + "select new PostDTO(" + + " p.id, " + + " p.title " + + ") " + + "from Post p " + + "where p.createdOn > :fromTimestamp", PostDTO.class) + .setParameter( + "fromTimestamp", + Timestamp.from( + LocalDate.of(2020, 1, 1) + .atStartOfDay() + .toInstant(ZoneOffset.UTC) + ) + ) + .getResultList(); + + assertEquals(1, postDTOs.size()); + }); + } + + @Entity(name = "Post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private Timestamp createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private Timestamp updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Timestamp getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Timestamp createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Timestamp getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(Timestamp updatedOn) { + this.updatedOn = updatedOn; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/dto/PostDTO.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/dto/PostDTO.java new file mode 100644 index 000000000..686806844 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/type/util/dto/PostDTO.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hibernate.type.util.dto; + +/** + * @author Vlad Mihalcea + */ +public class PostDTO { + + private Long id; + + private String title; + + public PostDTO(Number id, String title) { + this.id = id.longValue(); + this.title = title; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractMySQLIntegrationTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractMySQLIntegrationTest.java new file mode 100644 index 000000000..07fcc99d6 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractMySQLIntegrationTest.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hibernate.util; + +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.MySQLDataSourceProvider; + +/** + * AbstractMySQLIntegrationTest - Abstract MySQL IntegrationTest + * + * @author Vlad Mihalcea + */ +public abstract class AbstractMySQLIntegrationTest extends AbstractTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new MySQLDataSourceProvider(); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractOracleIntegrationTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractOracleIntegrationTest.java new file mode 100644 index 000000000..9e5808c44 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractOracleIntegrationTest.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hibernate.util; + +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.OracleDataSourceProvider; + +/** + * AbstractOracleIntegrationTest - Abstract Oracle IntegrationTest + * + * @author Vlad Mihalcea + */ +public abstract class AbstractOracleIntegrationTest extends AbstractTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new OracleDataSourceProvider(); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractPostgreSQLIntegrationTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractPostgreSQLIntegrationTest.java new file mode 100644 index 000000000..f181a97c3 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractPostgreSQLIntegrationTest.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hibernate.util; + +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.PostgreSQLDataSourceProvider; + +/** + * AbstractPostgreSQLIntegrationTest - Abstract PostgreSQL IntegrationTest + * + * @author Vlad Mihalcea + */ +public abstract class AbstractPostgreSQLIntegrationTest extends AbstractTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider(); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractSQLServerIntegrationTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractSQLServerIntegrationTest.java new file mode 100644 index 000000000..61ecde613 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractSQLServerIntegrationTest.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hibernate.util; + +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.SQLServerDataSourceProvider; + +/** + * AbstractSQLServerIntegrationTest - Abstract SQL Server IntegrationTest + * + * @author Vlad Mihalcea + */ +public abstract class AbstractSQLServerIntegrationTest extends AbstractTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new SQLServerDataSourceProvider(); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractTest.java new file mode 100644 index 000000000..eb0b13630 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractTest.java @@ -0,0 +1,535 @@ +package com.vladmihalcea.hibernate.util; + +import com.vladmihalcea.hibernate.util.providers.DataSourceProvider; +import com.vladmihalcea.hibernate.util.providers.HSQLDBDataSourceProvider; +import com.vladmihalcea.hibernate.util.transaction.*; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.EntityTransaction; +import jakarta.persistence.spi.PersistenceUnitInfo; +import org.hibernate.Interceptor; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; +import org.hibernate.boot.MetadataBuilder; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.SessionFactoryBuilder; +import org.hibernate.boot.model.TypeContributor; +import org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl; +import org.hibernate.boot.registry.BootstrapServiceRegistry; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; +import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.hibernate.type.BasicType; +import org.hibernate.type.Type; +import org.hibernate.usertype.UserType; +import org.junit.After; +import org.junit.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.io.Closeable; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +public abstract class AbstractTest { + + static { + Thread.currentThread().setName("Alice"); + } + + protected final ExecutorService executorService = Executors.newSingleThreadExecutor(r -> { + Thread bob = new Thread(r); + bob.setName("Bob"); + return bob; + }); + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + private EntityManagerFactory emf; + + private SessionFactory sf; + + private List closeables = new ArrayList<>(); + + @Before + public void init() { + if(nativeHibernateSessionFactoryBootstrap()) { + sf = newSessionFactory(); + } else { + emf = newEntityManagerFactory(); + } + afterInit(); + } + + protected void afterInit() { + + } + + @After + public void destroy() { + if (nativeHibernateSessionFactoryBootstrap()) { + sf.close(); + } else { + emf.close(); + } + for (Closeable closeable : closeables) { + try { + closeable.close(); + } catch (IOException e) { + LOGGER.error("Failure", e); + } + } + closeables.clear(); + } + + public EntityManagerFactory entityManagerFactory() { + return nativeHibernateSessionFactoryBootstrap() ? sf : emf; + } + + public SessionFactory sessionFactory() { + return nativeHibernateSessionFactoryBootstrap() ? sf : entityManagerFactory().unwrap(SessionFactory.class); + } + + protected boolean nativeHibernateSessionFactoryBootstrap() { + return false; + } + + protected abstract Class[] entities(); + + protected List entityClassNames() { + return Arrays.asList(entities()).stream().map(Class::getName).collect(Collectors.toList()); + } + + protected String[] packages() { + return null; + } + + protected String[] resources() { + return null; + } + + protected Interceptor interceptor() { + return null; + } + + private SessionFactory newSessionFactory() { + final BootstrapServiceRegistryBuilder bsrb = new BootstrapServiceRegistryBuilder() + .enableAutoClose(); + + Integrator integrator = integrator(); + if (integrator != null) { + bsrb.applyIntegrator(integrator); + } + + final BootstrapServiceRegistry bsr = bsrb.build(); + + final StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder(bsr) + .applySettings(properties()) + .build(); + + final MetadataSources metadataSources = new MetadataSources(serviceRegistry); + + for (Class annotatedClass : entities()) { + metadataSources.addAnnotatedClass(annotatedClass); + } + + String[] packages = packages(); + if (packages != null) { + for (String annotatedPackage : packages) { + metadataSources.addPackage(annotatedPackage); + } + } + + String[] resources = resources(); + if (resources != null) { + for (String resource : resources) { + metadataSources.addResource(resource); + } + } + + final MetadataBuilder metadataBuilder = metadataSources.getMetadataBuilder(); + metadataBuilder.applyImplicitNamingStrategy(ImplicitNamingStrategyLegacyJpaImpl.INSTANCE); + + final List additionalTypes = additionalTypes(); + if (additionalTypes != null) { + additionalTypes.stream().forEach(type -> { + metadataBuilder.applyTypes((typeContributions, serviceRegistry1) -> { + if(type instanceof BasicType) { + typeContributions.contributeType((BasicType) type); + } else if (type instanceof UserType ){ + typeContributions.contributeType((UserType) type); + } + }); + }); + } + + MetadataImplementor metadata = (MetadataImplementor) metadataBuilder.build(); + + final SessionFactoryBuilder sfb = metadata.getSessionFactoryBuilder(); + Interceptor interceptor = interceptor(); + if (interceptor != null) { + sfb.applyInterceptor(interceptor); + } + + return sfb.build(); + } + + private SessionFactory newLegacySessionFactory() { + Properties properties = properties(); + Configuration configuration = new Configuration().addProperties(properties); + for (Class entityClass : entities()) { + configuration.addAnnotatedClass(entityClass); + } + String[] packages = packages(); + if (packages != null) { + for (String scannedPackage : packages) { + configuration.addPackage(scannedPackage); + } + } + String[] resources = resources(); + if (resources != null) { + for (String resource : resources) { + configuration.addResource(resource); + } + } + Interceptor interceptor = interceptor(); + if (interceptor != null) { + configuration.setInterceptor(interceptor); + } + + final List additionalTypes = additionalTypes(); + if (additionalTypes != null) { + configuration.registerTypeContributor((typeContributions, serviceRegistry) -> { + additionalTypes.stream().forEach(type -> { + if (type instanceof BasicType) { + typeContributions.contributeType((BasicType) type); + } else if (type instanceof UserType) { + typeContributions.contributeType((UserType) type); + } + }); + }); + } + return configuration.buildSessionFactory( + new StandardServiceRegistryBuilder() + .applySettings(properties) + .build() + ); + } + + protected EntityManagerFactory newEntityManagerFactory() { + PersistenceUnitInfo persistenceUnitInfo = persistenceUnitInfo(getClass().getSimpleName()); + Map configuration = new HashMap<>(); + configuration.put(AvailableSettings.INTERCEPTOR, interceptor()); + Integrator integrator = integrator(); + if (integrator != null) { + configuration.put("hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList(integrator)); + } + + final List additionalTypes = additionalTypes(); + if (additionalTypes != null) { + configuration.put("hibernate.type_contributors", (TypeContributorList) () -> { + List typeContributors = new ArrayList<>(); + + for (Type additionalType : additionalTypes) { + if (additionalType instanceof BasicType) { + typeContributors.add((typeContributions, serviceRegistry) -> typeContributions.contributeType((BasicType) additionalType)); + + + } else if (additionalType instanceof UserType) { + typeContributors.add((typeContributions, serviceRegistry) -> typeContributions.contributeType((UserType) additionalType)); + } + } + return typeContributors; + }); + } + + EntityManagerFactoryBuilderImpl entityManagerFactoryBuilder = new EntityManagerFactoryBuilderImpl( + new PersistenceUnitInfoDescriptor(persistenceUnitInfo), configuration + ); + return entityManagerFactoryBuilder.build(); + } + + protected Integrator integrator() { + return null; + } + + protected PersistenceUnitInfoImpl persistenceUnitInfo(String name) { + PersistenceUnitInfoImpl persistenceUnitInfo = new PersistenceUnitInfoImpl( + name, entityClassNames(), properties() + ); + String[] resources = resources(); + if (resources != null) { + persistenceUnitInfo.getMappingFileNames().addAll(Arrays.asList(resources)); + } + return persistenceUnitInfo; + } + + protected Properties properties() { + Properties properties = new Properties(); + properties.put("hibernate.dialect", dataSourceProvider().hibernateDialect()); + //log settings + properties.put("hibernate.hbm2ddl.auto", "create-drop"); + //data source settings + DataSource dataSource = newDataSource(); + if (dataSource != null) { + properties.put("hibernate.connection.datasource", dataSource); + } + properties.put("hibernate.cache.ehcache.missing_cache_strategy", "create"); + additionalProperties(properties); + return properties; + } + + protected void additionalProperties(Properties properties) { + + } + + protected DataSourceProxyType dataSourceProxyType() { + return DataSourceProxyType.DATA_SOURCE_PROXY; + } + + protected DataSource newDataSource() { + DataSource dataSource = + proxyDataSource() + ? dataSourceProxyType().dataSource(dataSourceProvider().dataSource()) + : dataSourceProvider().dataSource(); + return dataSource; + } + + protected boolean proxyDataSource() { + return true; + } + + protected DataSourceProvider dataSourceProvider() { + return new HSQLDBDataSourceProvider(); + } + + protected List additionalTypes() { + return null; + } + + protected T doInHibernate(HibernateTransactionFunction callable) { + T result = null; + Session session = null; + Transaction txn = null; + try { + session = sessionFactory().openSession(); + callable.beforeTransactionCompletion(); + txn = session.beginTransaction(); + + result = callable.apply(session); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + callable.afterTransactionCompletion(); + if (session != null) { + session.close(); + } + } + return result; + } + + protected void doInHibernate(HibernateTransactionConsumer callable) { + Session session = null; + Transaction txn = null; + try { + session = sessionFactory().openSession(); + callable.beforeTransactionCompletion(); + txn = session.beginTransaction(); + + callable.accept(session); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + callable.afterTransactionCompletion(); + if (session != null) { + session.close(); + } + } + } + + protected T doInJPA(JPATransactionFunction function) { + T result = null; + EntityManager entityManager = null; + EntityTransaction txn = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + function.beforeTransactionCompletion(); + txn = entityManager.getTransaction(); + txn.begin(); + result = function.apply(entityManager); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + function.afterTransactionCompletion(); + if (entityManager != null) { + entityManager.close(); + } + } + return result; + } + + protected void doInJPA(JPATransactionVoidFunction function) { + EntityManager entityManager = null; + EntityTransaction txn = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + function.beforeTransactionCompletion(); + txn = entityManager.getTransaction(); + txn.begin(); + function.accept(entityManager); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + function.afterTransactionCompletion(); + if (entityManager != null) { + entityManager.close(); + } + } + } + + protected T doInJDBC(ConnectionCallable callable) { + AtomicReference result = new AtomicReference<>(); + Session session = null; + Transaction txn = null; + try { + session = sessionFactory().openSession(); + txn = session.beginTransaction(); + session.doWork(connection -> { + result.set(callable.execute(connection)); + }); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + if (session != null) { + session.close(); + } + } + return result.get(); + } + + protected void doInJDBC(ConnectionVoidCallable callable) { + Session session = null; + Transaction txn = null; + try { + session = sessionFactory().openSession(); + txn = session.beginTransaction(); + session.doWork(callable::execute); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + if (session != null) { + session.close(); + } + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/DataSourceProxyType.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/DataSourceProxyType.java new file mode 100644 index 000000000..1119252f1 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/DataSourceProxyType.java @@ -0,0 +1,32 @@ +package com.vladmihalcea.hibernate.util; + +import com.vladmihalcea.hibernate.util.logging.InlineQueryLogEntryCreator; +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.listener.DataSourceQueryCountListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; + +import javax.sql.DataSource; + +/** + * @author Vlad Mihalcea + */ +public enum DataSourceProxyType { + DATA_SOURCE_PROXY { + @Override + DataSource dataSource(DataSource dataSource) { + ChainListener listener = new ChainListener(); + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + listener.addListener(loggingListener); + listener.addListener(new DataSourceQueryCountListener()); + return ProxyDataSourceBuilder + .create(dataSource) + .name(name()) + .listener(listener) + .build(); + } + }; + + abstract DataSource dataSource(DataSource dataSource); +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/EntityProvider.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/EntityProvider.java new file mode 100644 index 000000000..ae8d50b99 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/EntityProvider.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hibernate.util; + +/** + * @author Vlad Mihalcea + */ +public interface EntityProvider { + + /** + * Entity types shared among multiple test configurations + * + * @return entity types + */ + Class[] entities(); +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/ExceptionUtil.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/ExceptionUtil.java new file mode 100644 index 000000000..332f36519 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/ExceptionUtil.java @@ -0,0 +1,129 @@ +package com.vladmihalcea.hibernate.util; + +import jakarta.persistence.LockTimeoutException; +import org.hibernate.PessimisticLockException; +import org.hibernate.exception.LockAcquisitionException; + +import java.sql.SQLTimeoutException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author Vlad Mihalcea + */ +public interface ExceptionUtil { + + List> LOCK_TIMEOUT_EXCEPTIONS = Arrays.asList( + LockAcquisitionException.class, + LockTimeoutException.class, + PessimisticLockException.class, + jakarta.persistence.PessimisticLockException.class, + SQLTimeoutException.class + ); + + /** + * Get the root cause of a particular {@code Throwable} + * + * @param t exception + * + * @return exception root cause + */ + static T rootCause(Throwable t) { + Throwable cause = t.getCause(); + if (cause != null && cause != t) { + return rootCause(cause); + } + return (T) t; + } + + /** + * Is the given throwable caused by a database lock timeout? + * + * @param e exception + * + * @return is caused by a database lock timeout + */ + static boolean isLockTimeout(Throwable e) { + AtomicReference causeHolder = new AtomicReference<>(e); + do { + final Throwable cause = causeHolder.get(); + final String failureMessage = cause.getMessage().toLowerCase(); + if (LOCK_TIMEOUT_EXCEPTIONS.stream().anyMatch(c -> c.isInstance(cause)) || + failureMessage.contains("timeout") || + failureMessage.contains("timed out") || + failureMessage.contains("time out") || + failureMessage.contains("closed connection") || + failureMessage.contains("link failure") + ) { + return true; + } else { + if (cause.getCause() == null || cause.getCause() == cause) { + break; + } else { + causeHolder.set(cause.getCause()); + } + } + } + while (true); + return false; + } + + /** + * Is the given throwable caused by a database MVCC anomaly detection? + * + * @param e exception + * + * @return is caused by a database lock MVCC anomaly detection + */ + static boolean isMVCCAnomalyDetection(Throwable e) { + AtomicReference causeHolder = new AtomicReference<>(e); + do { + final Throwable cause = causeHolder.get(); + if ( + cause.getMessage().contains("ORA-08177: can't serialize access for this transaction") //Oracle + || cause.getMessage().toLowerCase().contains("could not serialize access due to concurrent update") //PSQLException + || cause.getMessage().toLowerCase().contains("ould not serialize access due to read/write dependencies among transactions") //PSQLException + || cause.getMessage().toLowerCase().contains("snapshot isolation transaction aborted due to update conflict") //SQLServerException + ) { + return true; + } else { + if (cause.getCause() == null || cause.getCause() == cause) { + break; + } else { + causeHolder.set(cause.getCause()); + } + } + } + while (true); + return false; + } + + /** + * Was the given exception caused by a SQL connection close + * + * @param e exception + * + * @return is caused by a SQL connection close + */ + static boolean isConnectionClose(Exception e) { + Throwable cause = e; + do { + if (cause.getMessage().toLowerCase().contains("connection is close") + || cause.getMessage().toLowerCase().contains("closed connection") + || cause.getMessage().toLowerCase().contains("link failure") + || cause.getMessage().toLowerCase().contains("closed") + ) { + return true; + } else { + if (cause.getCause() == null || cause.getCause() == cause) { + break; + } else { + cause = cause.getCause(); + } + } + } + while (true); + return false; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/PersistenceUnitInfoImpl.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/PersistenceUnitInfoImpl.java new file mode 100644 index 000000000..22d1d6591 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/PersistenceUnitInfoImpl.java @@ -0,0 +1,134 @@ +package com.vladmihalcea.hibernate.util; + +import jakarta.persistence.SharedCacheMode; +import jakarta.persistence.ValidationMode; +import jakarta.persistence.spi.ClassTransformer; +import jakarta.persistence.spi.PersistenceUnitInfo; +import jakarta.persistence.spi.PersistenceUnitTransactionType; +import org.hibernate.jpa.HibernatePersistenceProvider; + +import javax.sql.DataSource; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class PersistenceUnitInfoImpl implements PersistenceUnitInfo { + + private final String persistenceUnitName; + private final List managedClassNames; + private final List mappingFileNames = new ArrayList<>(); + private final Properties properties; + private PersistenceUnitTransactionType transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL; + private DataSource jtaDataSource; + + private DataSource nonJtaDataSource; + + public PersistenceUnitInfoImpl(String persistenceUnitName, List managedClassNames, Properties properties) { + this.persistenceUnitName = persistenceUnitName; + this.managedClassNames = managedClassNames; + this.properties = properties; + } + + @Override + public String getPersistenceUnitName() { + return persistenceUnitName; + } + + @Override + public String getPersistenceProviderClassName() { + return HibernatePersistenceProvider.class.getName(); + } + + @Override + public PersistenceUnitTransactionType getTransactionType() { + return transactionType; + } + + @Override + public DataSource getJtaDataSource() { + return jtaDataSource; + } + + public PersistenceUnitInfoImpl setJtaDataSource(DataSource jtaDataSource) { + this.jtaDataSource = jtaDataSource; + this.nonJtaDataSource = null; + transactionType = PersistenceUnitTransactionType.JTA; + return this; + } + + @Override + public DataSource getNonJtaDataSource() { + return nonJtaDataSource; + } + + public PersistenceUnitInfoImpl setNonJtaDataSource(DataSource nonJtaDataSource) { + this.nonJtaDataSource = nonJtaDataSource; + this.jtaDataSource = null; + transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL; + return this; + } + + @Override + public List getMappingFileNames() { + return mappingFileNames; + } + + @Override + public List getJarFileUrls() { + return Collections.emptyList(); + } + + @Override + public URL getPersistenceUnitRootUrl() { + return null; + } + + @Override + public List getManagedClassNames() { + return managedClassNames; + } + + @Override + public boolean excludeUnlistedClasses() { + return false; + } + + @Override + public SharedCacheMode getSharedCacheMode() { + return SharedCacheMode.UNSPECIFIED; + } + + @Override + public ValidationMode getValidationMode() { + return ValidationMode.AUTO; + } + + public Properties getProperties() { + return properties; + } + + @Override + public String getPersistenceXMLSchemaVersion() { + return "2.1"; + } + + @Override + public ClassLoader getClassLoader() { + return Thread.currentThread().getContextClassLoader(); + } + + @Override + public void addTransformer(ClassTransformer transformer) { + + } + + @Override + public ClassLoader getNewTempClassLoader() { + return null; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/StringUtilsTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/StringUtilsTest.java new file mode 100644 index 000000000..0fb097d57 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/StringUtilsTest.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hibernate.util; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class StringUtilsTest { + + @Test + public void testJoin(){ + assertEquals( + "Oracle,PostgreSQL,MySQL,SQL Server", + StringUtils.join(",", "Oracle", "PostgreSQL", "MySQL", "SQL Server") + ); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/logging/InlineQueryLogEntryCreator.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/logging/InlineQueryLogEntryCreator.java new file mode 100644 index 000000000..a37c43029 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/logging/InlineQueryLogEntryCreator.java @@ -0,0 +1,85 @@ +package com.vladmihalcea.hibernate.util.logging; + +import net.ttddyy.dsproxy.ExecutionInfo; +import net.ttddyy.dsproxy.QueryInfo; +import net.ttddyy.dsproxy.listener.logging.DefaultQueryLogEntryCreator; + +import java.util.*; + +/** + * @author Vlad Mihalcea + */ +public class InlineQueryLogEntryCreator extends DefaultQueryLogEntryCreator { + @Override + protected void writeParamsEntry(StringBuilder sb, ExecutionInfo execInfo, List queryInfoList) { + sb.append("Params:["); + for (QueryInfo queryInfo : queryInfoList) { + boolean firstArg = true; + for (Map paramMap : queryInfo.getQueryArgsList()) { + + if (!firstArg) { + sb.append(", "); + } else { + firstArg = false; + } + + SortedMap sortedParamMap = new TreeMap(new CustomStringAsIntegerComparator()); + sortedParamMap.putAll(paramMap); + + sb.append("("); + boolean firstParam = true; + for (Map.Entry paramEntry : sortedParamMap.entrySet()) { + if (!firstParam) { + sb.append(", "); + } else { + firstParam = false; + } + Object parameter = paramEntry.getValue(); + if (parameter != null && parameter.getClass().isArray()) { + sb.append(arrayToString(parameter)); + } else { + sb.append(parameter); + } + } + sb.append(")"); + } + } + sb.append("]"); + } + + private String arrayToString(Object object) { + if (object.getClass().isArray()) { + if (object instanceof byte[]) { + return Arrays.toString((byte[]) object); + } + if (object instanceof short[]) { + return Arrays.toString((short[]) object); + } + if (object instanceof char[]) { + return Arrays.toString((char[]) object); + } + if (object instanceof int[]) { + return Arrays.toString((int[]) object); + } + if (object instanceof long[]) { + return Arrays.toString((long[]) object); + } + if (object instanceof float[]) { + return Arrays.toString((float[]) object); + } + if (object instanceof double[]) { + return Arrays.toString((double[]) object); + } + if (object instanceof boolean[]) { + return Arrays.toString((boolean[]) object); + } + if (object instanceof Object[]) { + return Arrays.toString((Object[]) object); + } + } + throw new UnsupportedOperationException("Array type not supported: " + object.getClass()); + } + + private static class CustomStringAsIntegerComparator extends StringAsIntegerComparator { + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/DataSourceProvider.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/DataSourceProvider.java new file mode 100644 index 000000000..2f5e28a49 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/DataSourceProvider.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hibernate.util.providers; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public interface DataSourceProvider { + + String hibernateDialect(); + + DataSource dataSource(); + + Class dataSourceClassName(); + + Properties dataSourceProperties(); + + String url(); + + String username(); + + String password(); + + Database database(); + + enum IdentifierStrategy { + IDENTITY, + SEQUENCE + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/Database.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/Database.java new file mode 100644 index 000000000..9dd694070 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/Database.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hibernate.util.providers; + +/** + * @author Vlad Mihalcea + */ +public enum Database { + HSQLDB, + H2, + POSTGRESQL, + ORACLE, + MYSQL, + SQLSERVER, + COCKROACHDB +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/H2DataSourceProvider.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/H2DataSourceProvider.java new file mode 100644 index 000000000..95c249ead --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/H2DataSourceProvider.java @@ -0,0 +1,61 @@ +package com.vladmihalcea.hibernate.util.providers; + +import org.h2.jdbcx.JdbcConnectionPool; +import org.h2.jdbcx.JdbcDataSource; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class H2DataSourceProvider implements DataSourceProvider { + + @Override + public String hibernateDialect() { + return "org.hibernate.dialect.H2Dialect"; + } + + @Override + public DataSource dataSource() { + return JdbcConnectionPool.create( + url(), + username(), + password() + ); + } + + @Override + public Class dataSourceClassName() { + return JdbcDataSource.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("url", url()); + properties.setProperty("user", username()); + properties.setProperty("password", password()); + return properties; + } + + @Override + public String url() { + return "jdbc:h2:mem:test"; + } + + @Override + public String username() { + return "sa"; + } + + @Override + public String password() { + return ""; + } + + @Override + public Database database() { + return Database.H2; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/HSQLDBDataSourceProvider.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/HSQLDBDataSourceProvider.java new file mode 100644 index 000000000..f7a4f9117 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/HSQLDBDataSourceProvider.java @@ -0,0 +1,60 @@ +package com.vladmihalcea.hibernate.util.providers; + +import org.hsqldb.jdbc.JDBCDataSource; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class HSQLDBDataSourceProvider implements DataSourceProvider { + + @Override + public String hibernateDialect() { + return "org.hibernate.dialect.HSQLDialect"; + } + + @Override + public DataSource dataSource() { + JDBCDataSource dataSource = new JDBCDataSource(); + dataSource.setUrl(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return JDBCDataSource.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("url", url()); + properties.setProperty("user", username()); + properties.setProperty("password", password()); + return properties; + } + + @Override + public String url() { + return "jdbc:hsqldb:mem:test"; + } + + @Override + public String username() { + return "sa"; + } + + @Override + public String password() { + return ""; + } + + @Override + public Database database() { + return Database.HSQLDB; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/MySQLDataSourceProvider.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/MySQLDataSourceProvider.java new file mode 100644 index 000000000..669fbb2c6 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/MySQLDataSourceProvider.java @@ -0,0 +1,138 @@ +package com.vladmihalcea.hibernate.util.providers; + +import com.mysql.cj.jdbc.MysqlDataSource; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class MySQLDataSourceProvider implements DataSourceProvider { + + private boolean rewriteBatchedStatements = true; + + private boolean cachePrepStmts = false; + + private boolean useServerPrepStmts = false; + + private boolean useTimezone = false; + + private boolean useJDBCCompliantTimezoneShift = false; + + private boolean useLegacyDatetimeCode = true; + + public boolean isRewriteBatchedStatements() { + return rewriteBatchedStatements; + } + + public void setRewriteBatchedStatements(boolean rewriteBatchedStatements) { + this.rewriteBatchedStatements = rewriteBatchedStatements; + } + + public boolean isCachePrepStmts() { + return cachePrepStmts; + } + + public void setCachePrepStmts(boolean cachePrepStmts) { + this.cachePrepStmts = cachePrepStmts; + } + + public boolean isUseServerPrepStmts() { + return useServerPrepStmts; + } + + public void setUseServerPrepStmts(boolean useServerPrepStmts) { + this.useServerPrepStmts = useServerPrepStmts; + } + + public boolean isUseTimezone() { + return useTimezone; + } + + public void setUseTimezone(boolean useTimezone) { + this.useTimezone = useTimezone; + } + + public boolean isUseJDBCCompliantTimezoneShift() { + return useJDBCCompliantTimezoneShift; + } + + public void setUseJDBCCompliantTimezoneShift(boolean useJDBCCompliantTimezoneShift) { + this.useJDBCCompliantTimezoneShift = useJDBCCompliantTimezoneShift; + } + + public boolean isUseLegacyDatetimeCode() { + return useLegacyDatetimeCode; + } + + public void setUseLegacyDatetimeCode(boolean useLegacyDatetimeCode) { + this.useLegacyDatetimeCode = useLegacyDatetimeCode; + } + + @Override + public String hibernateDialect() { + return "org.hibernate.dialect.MySQL57Dialect"; + } + + @Override + public DataSource dataSource() { + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setURL("jdbc:mysql://localhost/high_performance_java_persistence?useSSL=false&" + + "rewriteBatchedStatements=" + rewriteBatchedStatements + + "&cachePrepStmts=" + cachePrepStmts + + "&useServerPrepStmts=" + useServerPrepStmts + + "&useTimezone=" + useTimezone + + "&useJDBCCompliantTimezoneShift=" + useJDBCCompliantTimezoneShift + + "&useLegacyDatetimeCode=" + useLegacyDatetimeCode + + ); + dataSource.setUser("mysql"); + dataSource.setPassword("admin"); + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return MysqlDataSource.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("url", url()); + return properties; + } + + @Override + public String url() { + return "jdbc:mysql://localhost/high_performance_java_persistence?user=mysql&password=admin"; + } + + @Override + public String username() { + return null; + } + + @Override + public String password() { + return null; + } + + @Override + public Database database() { + return Database.MYSQL; + } + + @Override + public String toString() { + return "MySQLDataSourceProvider{" + + "rewriteBatchedStatements=" + rewriteBatchedStatements + + ", cachePrepStmts=" + cachePrepStmts + + ", useServerPrepStmts=" + useServerPrepStmts + + ", useTimezone=" + useTimezone + + ", useJDBCCompliantTimezoneShift=" + useJDBCCompliantTimezoneShift + + ", useLegacyDatetimeCode=" + useLegacyDatetimeCode + + '}'; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/OracleDataSourceProvider.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/OracleDataSourceProvider.java new file mode 100644 index 000000000..92e0d3d9a --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/OracleDataSourceProvider.java @@ -0,0 +1,66 @@ +package com.vladmihalcea.hibernate.util.providers; + +import com.vladmihalcea.hibernate.util.ReflectionUtils; +import org.hibernate.dialect.OracleDialect; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class OracleDataSourceProvider implements DataSourceProvider { + @Override + public String hibernateDialect() { + return OracleDialect.class.getName(); + } + + @Override + public DataSource dataSource() { + try { + DataSource dataSource = ReflectionUtils.newInstance("oracle.jdbc.pool.OracleDataSource"); + ReflectionUtils.invokeSetter(dataSource, "databaseName", "high_performance_java_persistence"); + ReflectionUtils.invokeSetter(dataSource, "URL", url()); + ReflectionUtils.invokeSetter(dataSource, "user", "oracle"); + ReflectionUtils.invokeSetter(dataSource, "password", "admin"); + return dataSource; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Override + public Class dataSourceClassName() { + return ReflectionUtils.getClass("oracle.jdbc.pool.OracleDataSource"); + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("databaseName", "high_performance_java_persistence"); + properties.setProperty("URL", url()); + properties.setProperty("user", username()); + properties.setProperty("password", password()); + return properties; + } + + @Override + public String url() { + return "jdbc:oracle:thin:@localhost:1521/xe"; + } + + @Override + public String username() { + return "oracle"; + } + + @Override + public String password() { + return "admin"; + } + + @Override + public Database database() { + return Database.ORACLE; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/PostgreSQLDataSourceProvider.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/PostgreSQLDataSourceProvider.java new file mode 100644 index 000000000..8dadc37b1 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/PostgreSQLDataSourceProvider.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hibernate.util.providers; + +import org.hibernate.dialect.PostgreSQL95Dialect; +import org.postgresql.ds.PGSimpleDataSource; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLDataSourceProvider implements DataSourceProvider { + + @Override + public String hibernateDialect() { + return PostgreSQL95Dialect.class.getName(); + } + + @Override + public DataSource dataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setDatabaseName("high_performance_java_persistence"); + dataSource.setServerName("localhost"); + dataSource.setUser("postgres"); + dataSource.setPassword("admin"); + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return PGSimpleDataSource.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("databaseName", "high_performance_java_persistence"); + properties.setProperty("serverName", "localhost"); + properties.setProperty("user", username()); + properties.setProperty("password", password()); + return properties; + } + + @Override + public String url() { + return null; + } + + @Override + public String username() { + return "postgres"; + } + + @Override + public String password() { + return "admin"; + } + + @Override + public Database database() { + return Database.POSTGRESQL; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/SQLServerDataSourceProvider.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/SQLServerDataSourceProvider.java new file mode 100644 index 000000000..62a1ace79 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/providers/SQLServerDataSourceProvider.java @@ -0,0 +1,60 @@ +package com.vladmihalcea.hibernate.util.providers; + +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerDataSourceProvider implements DataSourceProvider { + @Override + public String hibernateDialect() { + return "org.hibernate.dialect.SQLServer2012Dialect"; + } + + @Override + public DataSource dataSource() { + SQLServerDataSource dataSource = new SQLServerDataSource(); + dataSource.setURL( + "jdbc:sqlserver://localhost;instance=SQLEXPRESS;" + + "databaseName=high_performance_java_persistence;" + ); + dataSource.setUser("sa"); + dataSource.setPassword("adm1n"); + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return SQLServerDataSource.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty( "URL", url() ); + return properties; + } + + @Override + public String url() { + return "jdbc:sqlserver://localhost;instance=SQLEXPRESS;databaseName=high_performance_java_persistence;user=sa;password=adm1n"; + } + + @Override + public String username() { + return "sa"; + } + + @Override + public String password() { + return "adm1n"; + } + + @Override + public Database database() { + return Database.SQLSERVER; + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/ConnectionCallable.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/ConnectionCallable.java new file mode 100644 index 000000000..c76548d1a --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/ConnectionCallable.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hibernate.util.transaction; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface ConnectionCallable { + T execute(Connection connection) throws SQLException; +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/ConnectionVoidCallable.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/ConnectionVoidCallable.java new file mode 100644 index 000000000..de2b9a657 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/ConnectionVoidCallable.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hibernate.util.transaction; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface ConnectionVoidCallable { + void execute(Connection connection) throws SQLException; +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/HibernateTransactionConsumer.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/HibernateTransactionConsumer.java new file mode 100644 index 000000000..5501ef254 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/HibernateTransactionConsumer.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hibernate.util.transaction; + +import org.hibernate.Session; + +import java.util.function.Consumer; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface HibernateTransactionConsumer extends Consumer { + default void beforeTransactionCompletion() { + + } + + default void afterTransactionCompletion() { + + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/HibernateTransactionFunction.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/HibernateTransactionFunction.java new file mode 100644 index 000000000..74d88f677 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/HibernateTransactionFunction.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hibernate.util.transaction; + +import org.hibernate.Session; + +import java.util.function.Function; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface HibernateTransactionFunction extends Function { + default void beforeTransactionCompletion() { + + } + + default void afterTransactionCompletion() { + + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/JPATransactionFunction.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/JPATransactionFunction.java new file mode 100644 index 000000000..78d5010ee --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/JPATransactionFunction.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hibernate.util.transaction; + +import jakarta.persistence.EntityManager; + +import java.util.function.Function; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface JPATransactionFunction extends Function { + default void beforeTransactionCompletion() { + + } + + default void afterTransactionCompletion() { + + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/JPATransactionVoidFunction.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/JPATransactionVoidFunction.java new file mode 100644 index 000000000..82822cbf5 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/JPATransactionVoidFunction.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hibernate.util.transaction; + +import jakarta.persistence.EntityManager; + +import java.util.function.Consumer; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface JPATransactionVoidFunction extends Consumer { + default void beforeTransactionCompletion() { + + } + + default void afterTransactionCompletion() { + + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/VoidCallable.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/VoidCallable.java new file mode 100644 index 000000000..bdbfff7cb --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/transaction/VoidCallable.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hibernate.util.transaction; + +import java.util.concurrent.Callable; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface VoidCallable extends Callable { + + void execute(); + + default Void call() throws Exception { + execute(); + return null; + } +} diff --git a/hibernate-types-60/src/test/resources/PostgreSQLJsonBinaryTypeConfigurationTest.properties b/hibernate-types-60/src/test/resources/PostgreSQLJsonBinaryTypeConfigurationTest.properties new file mode 100644 index 000000000..a004d9d2e --- /dev/null +++ b/hibernate-types-60/src/test/resources/PostgreSQLJsonBinaryTypeConfigurationTest.properties @@ -0,0 +1 @@ +hibernate.types.jackson.object.mapper=com.vladmihalcea.hibernate.type.json.configuration.CustomObjectMapperSupplier \ No newline at end of file diff --git a/hibernate-types-60/src/test/resources/PostgreSQLJsonBinaryTypeCustomJsonSerializerConfigurationTest.properties b/hibernate-types-60/src/test/resources/PostgreSQLJsonBinaryTypeCustomJsonSerializerConfigurationTest.properties new file mode 100644 index 000000000..783f20454 --- /dev/null +++ b/hibernate-types-60/src/test/resources/PostgreSQLJsonBinaryTypeCustomJsonSerializerConfigurationTest.properties @@ -0,0 +1 @@ +hibernate.types.json.serializer=com.vladmihalcea.hibernate.type.json.configuration.CustomJsonSerializerSupplier \ No newline at end of file diff --git a/hibernate-types-60/src/test/resources/application.properties b/hibernate-types-60/src/test/resources/application.properties new file mode 100644 index 000000000..c960d79f9 --- /dev/null +++ b/hibernate-types-60/src/test/resources/application.properties @@ -0,0 +1 @@ +hibernate.types.app.props=true \ No newline at end of file diff --git a/hibernate-types-60/src/test/resources/hibernate-types.properties b/hibernate-types-60/src/test/resources/hibernate-types.properties new file mode 100644 index 000000000..ceba8af08 --- /dev/null +++ b/hibernate-types-60/src/test/resources/hibernate-types.properties @@ -0,0 +1 @@ +hibernate.types.def=ghi \ No newline at end of file diff --git a/hibernate-types-60/src/test/resources/hibernate.properties b/hibernate-types-60/src/test/resources/hibernate.properties new file mode 100644 index 000000000..d3014a3da --- /dev/null +++ b/hibernate-types-60/src/test/resources/hibernate.properties @@ -0,0 +1,3 @@ +hibernate.types.print.banner=false +hibernate.types.abc=def +hibernate.types.def=def diff --git a/hibernate-types-60/src/test/resources/logback-test.xml b/hibernate-types-60/src/test/resources/logback-test.xml new file mode 100644 index 000000000..1a284d9cd --- /dev/null +++ b/hibernate-types-60/src/test/resources/logback-test.xml @@ -0,0 +1,23 @@ + + + + ${console.log.level} + + + %-5p [%t]: %c{1} - %m%n + UTF-8 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index bbcc8dde1..867e935bd 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,7 @@ + hibernate-types-60 hibernate-types-55 hibernate-types-52 hibernate-types-5 @@ -334,7 +335,7 @@ UTF-8 1.1 - 3.3 + 3.8.0 3.0.2 2.2 3.1.0