Compare commits

..
Author SHA1 Message Date
Robin Bygrave 87a7d4958e [maven-release-plugin] prepare release avaje-ebeanorm-6.10.3 2015-11-04 22:10:13 +13:00
Robin Bygrave 511ebd8a29 #450 - @ManyToMany with cascade and jdbc batch - ensure flush occurs 2015-11-03 16:31:27 +13:00
Robin Bygrave 11f4c8d7a3 #449 - Remove whitespace from generated Postgres trigger DDL (as Play evolution not parsing it) 2015-11-03 16:28:00 +13:00
Robin Bygrave d417a92843 [maven-release-plugin] prepare for next development iteration 2015-11-02 16:42:33 +13:00
Robin Bygrave 4bd5e52df3 [maven-release-plugin] prepare release avaje-ebeanorm-6.10.2 2015-11-02 16:42:10 +13:00
Robin Bygrave 788a200d16 #445 - Multiple @*ToMany relations referencing the same type 2015-11-02 14:11:23 +13:00
Robin Bygrave e18d2004dc #448 - ENH: Postgres Support for UUID mapping to native DB UUID type (relating to #430) 2015-11-02 12:57:47 +13:00
Robin Bygrave b326724139 #443 - ENH: Enum mapping - Add annotation @DbEnumValue that can be used to on a enum method which provides DB values 2015-10-30 21:42:21 +13:00
Robin Bygrave fc0b47342b #436 - @DiscriminatorColumn mapped also as @Column leads to NullPointerException 2015-10-29 22:01:28 +13:00
Robin Bygrave ec1a68a133 #435 - Duplicate foreign key name with ManyToMany relations 2015-10-29 17:44:29 +13:00
Robin Bygrave 8ca12521d7 No effective change - reduce debug level 2015-10-29 17:43:54 +13:00
Robin Bygrave 2362ef4130 [maven-release-plugin] prepare for next development iteration 2015-10-29 09:01:17 +13:00
Robin Bygrave 7e233edcc3 [maven-release-plugin] prepare release avaje-ebeanorm-6.10.1 2015-10-29 09:00:55 +13:00
Robin Bygrave a8b98282a1 Bump pom to 6.10.1-SNAPSHOT 2015-10-29 09:00:00 +13:00
Robin Bygrave b4506aa718 #444 - Spring Boot classpath scanning not finding entity beans - part2 - embedded jar scanning 2015-10-29 08:59:06 +13:00
Robin Bygrave 848a3b8888 #444 - Spring Boot classpath scanning not finding entity beans 2015-10-28 15:45:09 +13:00
Robin Bygrave 67c711d75f #442 - @JsonIgnore / @Expose ... not working on @OneToMany or @ManyToMany - was - @Transient field on @MappedSuperclass is not ignored by toJson() 2015-10-28 09:10:19 +13:00
Robin Bygrave 3d62f58930 #439 - @EnumValue("01") stored as integer rather than string - ENum stores strings as numerics? 2015-10-28 08:53:14 +13:00
Robin Bygrave a30948d38d #438 - findPagedList and getTotalRowCount, the query.getAlias() is null 2015-10-28 08:15:15 +13:00
Robin Bygrave a8232d2d8b #441 - ENH: Add support for test-ebean.properties ... for mechanism to configure ebean to run tests 2015-10-27 00:17:47 +13:00
Robin Bygrave d4b8f23a5a Fix test - MySql limitations with delete query syntax 2015-10-26 21:52:35 +13:00
Robin Bygrave bcf2a44187 #430 - ENH: Support for UUID mapping to native DB UUID type (Postgres, H2) 2015-10-26 20:52:16 +13:00
Robin Bygrave f3102ba02c #432 - Delete using Query does not commit 2015-10-22 13:29:21 +13:00
Robin Bygrave 0351eab3a7 #431 - Annotation PrivateOwned seems not enough to update an change in the list of children (fix) 2015-10-22 11:10:41 +13:00
Robin Bygrave 409e2b75ce #431 - Add failing test ... Annotation PrivateOwned seems not enough to update an change in the list of children 2015-10-22 09:27:32 +13:00
Robin Bygrave dc968f16bf Q: Ebean @ManyToMany(cascade=REMOVE) described #429 2015-10-06 14:38:05 +13:00
Robin Bygrave bbbb6482fb [maven-release-plugin] prepare for next development iteration 2015-10-02 13:03:34 +13:00
Robin Bygrave 001968fcf8 [maven-release-plugin] prepare release avaje-ebeanorm-6.9.1 2015-10-02 13:03:07 +13:00
Robin Bygrave 6abcb186a4 Bump pom to 6.9.1-SNAPSHOT 2015-10-02 13:02:13 +13:00
Robin Bygrave f250447fbe Add test for ManyToMany delete scenario 2015-10-01 22:21:47 +13:00
Robin Bygrave bfa86f241b #427 - ENH: Add support for Removing ShutdownManager.shutdownHook ... manually control entire shutdown. 2015-10-01 10:33:09 +13:00
Robin Bygrave 19459b71b4 Fix for #425 - Extra instance of root level bean with findIterate() and fetch of OneToMany relationship - Ebean parses the first bean correctly and the rest not 2015-09-30 23:35:10 +13:00
Robin Bygrave 5e97c3bbbc #425 - Extra instance of root level bean with findIterate() and fetch of OneToMany relationship - Ebean parses the first bean correctly and the rest not 2015-09-30 23:24:53 +13:00
Robin Bygrave 986eadd023 #424 - DDL generation - support turning off lower casing of table and column names DbConstraintNaming, DbConstraintNormalise 2015-09-30 22:44:47 +13:00
Robin Bygrave c6787d0901 Update javadoc 2015-09-30 17:26:30 +13:00
Robin Bygrave 3792e06859 Update javadoc 2015-09-30 17:26:05 +13:00
Robin Bygrave c77446ba24 Update javadoc 2015-09-30 17:25:26 +13:00
Robin Bygrave dba245c517 #421 - ENH: Add support for unsetting the loaded state of a bean property (via BeanState) 2015-09-22 13:21:19 +12:00
Robin Bygrave 489eb483c0 No effective change - parameter name from val -> value 2015-09-22 12:34:30 +12:00
Robin Bygrave 11301846be #422 - ENH: Add jsonBetween() expression 2015-09-22 12:32:46 +12:00
Robin Bygrave d022e4aec8 No effective change - parameter name from val -> value 2015-09-22 11:48:41 +12:00
Robin Bygrave aa788edb68 #421 - ENH: Add support for unsetting the loaded state of a bean property (via BeanState) ... initial test 2015-09-22 09:00:06 +12:00
Robin Bygrave 288adcdf68 Update README 2015-09-21 18:01:31 +12:00
Robin Bygrave ddb07ca202 Update README 2015-09-21 17:59:45 +12:00
Robin Bygrave 524b592e82 [maven-release-plugin] prepare for next development iteration 2015-09-21 17:45:47 +12:00
Robin Bygrave 0124f0f254 [maven-release-plugin] prepare release avaje-ebeanorm-6.8.1 2015-09-21 17:45:21 +12:00
Robin Bygrave eddd6a36d6 No effective change - javadoc only 2015-09-21 17:36:13 +12:00
Robin Bygrave 91e309d770 Bump pom version to 6.8.1-SNAPSHOT 2015-09-21 17:03:22 +12:00
Robin Bygrave 5726410f6f #417 - ENH: Add JSON expressions ... path exists and value at path equals value 2015-09-21 16:20:14 +12:00
Robin Bygrave 054f7dc86f #415 - ENH: Add ExplicitJdbcTransaction ... so like AutoCommitJdbcTransaction but using explicit statements to commit and rollback 2015-09-18 22:25:19 +12:00
Robin Bygrave 06b5ebc14e #419 - Support running History DDL scripts in full drop and create ddl execution - detect $$ procedures/triggers in ddl script 2015-09-18 22:19:52 +12:00
Robin Bygrave f6ee82428f Change test - disable this test for MySql - it does not like trying to delete using sub query select from same table 2015-09-18 22:18:37 +12:00
Robin Bygrave e1d45dd79d #419 - Support running History DDL scripts in full drop and create ddl execution - detect $$ procedures/triggers in ddl script 2015-09-18 22:03:26 +12:00
Robin Bygrave 08c455279f #418 - Postgres History DDL generation - invalid update table statement to initialise the sys_period 2015-09-18 21:30:59 +12:00
Robin Bygrave 32dcbc6703 Change test to only run with H2 (but it would also run with db's that use history table views/triggers but not with oracle total recall) 2015-09-18 21:13:19 +12:00
Robin Bygrave c9e733df4e Fix test - incorrect raw sql syntax (breaks in Oracle) 2015-09-18 21:06:54 +12:00
Robin Bygrave 606e1e6d3a #414 - Enh info: @History support for H2 2015-09-18 17:16:46 +12:00
Robin Bygrave fcb50ea13a No effective change - remove whitespace 2015-09-18 11:45:19 +12:00
Robin Bygrave 5b26e77cfd #413 - ENH: Add ability to clear the OrderBy ... so that a default order by can be set in code and then cleared / overwritten 2015-09-18 11:39:47 +12:00
Robin Bygrave 9fc5d8a201 Pom - bump test Postgres JDBC driver version 2015-09-17 11:33:09 +12:00
Robin Bygrave ba78c5e904 [maven-release-plugin] prepare for next development iteration 2015-09-14 16:19:59 +12:00
118 changed files with 3270 additions and 404 deletions
+1 -10
View File
@@ -5,16 +5,7 @@
Ebean ORM
==============
[http://ebean-orm.github.io/](http://ebean-orm.github.io/ "Ebean ORM's Website")
Maven Dependency
----------------
<dependency>
<groupId>org.avaje.ebeanorm</groupId>
<artifactId>avaje-ebeanorm</artifactId>
<version>4.7.3</version>
</dependency>
[https://ebean-orm.github.io/](http://ebean-orm.github.io/ "Ebean ORM's Website")
[Maven central - avaje-ebeanorm](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.avaje.ebeanorm%22%20AND%20a%3A%22avaje-ebeanorm%22 "maven central ebeanorm")
+18 -8
View File
@@ -9,7 +9,7 @@
<groupId>org.avaje.ebeanorm</groupId>
<artifactId>avaje-ebeanorm</artifactId>
<version>6.7.1</version>
<version>6.10.3</version>
<packaging>jar</packaging>
<name>avaje-ebeanorm</name>
@@ -61,6 +61,14 @@
<version>1.7.7</version>
</dependency>
<!-- provided scope for Spring boot loader classpath scanning support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>1.2.7.RELEASE</version>
<scope>provided</scope>
</dependency>
<!-- provided scope for JsonNode support -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
@@ -109,7 +117,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.4-1201-jdbc41</version>
<version>9.4-1202-jdbc41</version>
<scope>provided</scope>
</dependency>
@@ -147,12 +155,14 @@
<version>4.7.1</version>
<scope>test</scope>
</dependency>
<!-- Provided scope so that the H2HistoryTrigger can live in Ebean core
and not require a separate module for it -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.182</version>
<scope>test</scope>
<version>1.4.189</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -218,7 +228,7 @@
<configuration>
<classSource>target/test-classes</classSource>
<packages>com.avaje.tests.**</packages>
<transformArgs>debug=2</transformArgs>
<transformArgs>debug=1</transformArgs>
</configuration>
<goals>
<goal>enhance</goal>
@@ -263,11 +273,11 @@
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
<configuration>
<doctitle>Ebean 4</doctitle>
<doctitle>Ebean 6</doctitle>
<overview>src/main/java/com/avaje/ebean/overview.html</overview>
<source>1.8</source>
<doclet>org.avaje.doclet.PygmentsDoclet</doclet>
<excludePackageNames>com.avaje.ebeaninternal.*:com.avaje.ebean.util</excludePackageNames>
<excludePackageNames>com.avaje.ebeaninternal.*:com.avaje.ebean.util:com.avaje.ebean.dbmigration.ddlgeneration:com.avaje.ebean.dbmigration.migration:com.avaje.ebean.dbmigration.migrationreader:com.avaje.ebean.dbmigration.model</excludePackageNames>
<docletArtifact>
<groupId>org.avaje</groupId>
<artifactId>pygments-doclet</artifactId>
@@ -38,6 +38,33 @@ public interface BeanState {
*/
void setDisableLazyLoad(boolean disableLazyLoading);
/**
* Set the loaded state of the property given it's name.
*
* <p>
* Typically this would be used to set the loaded state of a property
* to false to ensure that the specific property is excluded from a
* stateless update.
* </p>
*
* <pre>{@code
*
* // populate a bean via say JSON
* User user = ...;
*
* // set loaded state on the email property to false so that
* // the email property is not included in a stateless update
* Ebean.getBeanState(user).setPropertyLoaded("email", false);
*
* user.update();
*
* }</pre>
*
*
* This will throw an IllegalArgumentException if the property is unknown.
*/
void setPropertyLoaded(String propertyName, boolean loaded);
/**
* For partially populated beans returns the properties that are loaded on the
* bean.
@@ -36,6 +36,51 @@ import java.util.Map;
*/
public interface ExpressionFactory {
/**
* Path exists - for the given path in a JSON document.
*/
Expression jsonExists(String propertyName, String path);
/**
* Path does not exist - for the given path in a JSON document.
*/
Expression jsonNotExists(String propertyName, String path);
/**
* Equal to - for the given path in a JSON document.
*/
Expression jsonEqualTo(String propertyName, String path, Object val);
/**
* Not Equal to - for the given path in a JSON document.
*/
Expression jsonNotEqualTo(String propertyName, String path, Object val);
/**
* Greater than - for the given path in a JSON document.
*/
Expression jsonGreaterThan(String propertyName, String path, Object val);
/**
* Greater than or equal to - for the given path in a JSON document.
*/
Expression jsonGreaterOrEqual(String propertyName, String path, Object val);
/**
* Less than - for the given path in a JSON document.
*/
Expression jsonLessThan(String propertyName, String path, Object val);
/**
* Less than or equal to - for the given path in a JSON document.
*/
Expression jsonLessOrEqualTo(String propertyName, String path, Object val);
/**
* Between - for the given path in a JSON document.
*/
Expression jsonBetween(String propertyName, String path, Object lowerValue, Object upperValue);
/**
* Equal To - property equal to the given value.
*/
@@ -345,12 +345,125 @@ public interface ExpressionList<T> extends Serializable {
*/
ExpressionList<T> where();
/**
* Path exists - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonExists("content", "path.other")
*
* }</pre>
*
* @param propertyName the property that holds a JSON document
* @param path the nested path in the JSON document in dot notation
*/
ExpressionList<T> jsonExists(String propertyName, String path);
/**
* Path does not exist - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonNotExists("content", "path.other")
*
* }</pre>
*
* @param propertyName the property that holds a JSON document
* @param path the nested path in the JSON document in dot notation
*/
ExpressionList<T> jsonNotExists(String propertyName, String path);
/**
* Equal to expression for the value at the given path in the JSON document.
*
* <pre>{@code
*
* where().jsonEqualTo("content", "path.other", 34)
*
* }</pre>
*
* @param propertyName the property that holds a JSON document
* @param path the nested path in the JSON document in dot notation
* @param value the value used to test against the document path's value
*/
ExpressionList<T> jsonEqualTo(String propertyName, String path, Object value);
/**
* Not Equal to - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonNotEqualTo("content", "path.other", 34)
*
* }</pre>
*
* @param propertyName the property that holds a JSON document
* @param path the nested path in the JSON document in dot notation
* @param value the value used to test against the document path's value
*/
ExpressionList<T> jsonNotEqualTo(String propertyName, String path, Object value);
/**
* Greater than - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonGreaterThan("content", "path.other", 34)
*
* }</pre>
*/
ExpressionList<T> jsonGreaterThan(String propertyName, String path, Object value);
/**
* Greater than or equal to - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonGreaterOrEqual("content", "path.other", 34)
*
* }</pre>
*/
ExpressionList<T> jsonGreaterOrEqual(String propertyName, String path, Object value);
/**
* Less than - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonLessThan("content", "path.other", 34)
*
* }</pre>
*/
ExpressionList<T> jsonLessThan(String propertyName, String path, Object value);
/**
* Less than or equal to - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonLessOrEqualTo("content", "path.other", 34)
*
* }</pre>
*/
ExpressionList<T> jsonLessOrEqualTo(String propertyName, String path, Object value);
/**
* Between - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonBetween("content", "orderDate", lowerDateTime, upperDateTime)
*
* }</pre>
*/
ExpressionList<T> jsonBetween(String propertyName, String path, Object lowerValue, Object upperValue);
/**
* Add an Expression to the list.
* <p>
* This returns the list so that add() can be chained.
* </p>
*
*
* <pre>{@code
*
* Query<Customer> query = Ebean.find(Customer.class);
+26 -1
View File
@@ -1,5 +1,6 @@
package com.avaje.ebean;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.text.PathProperties;
import com.avaje.ebean.util.ClassUtil;
import org.jetbrains.annotations.Nullable;
@@ -193,7 +194,31 @@ public abstract class Model {
public void markAsDirty() {
db().markAsDirty(this);
}
/**
* Mark the property as unset or 'not loaded'.
* <p>
* This would be used to specify a property that we did not wish to include in a stateless update.
* </p>
* <pre>{@code
*
* // populate an entity bean from JSON or whatever
* User user = ...;
*
* // mark the email property as 'unset' so that it is not
* // included in a 'stateless update'
* user.markPropertyUnset("email");
*
* user.update();
*
* }</pre>
*
* @param propertyName the name of the property on the bean to be marked as 'unset'
*/
public void markPropertyUnset(String propertyName) {
((EntityBean)this)._ebean_getIntercept().setPropertyLoaded(propertyName, false);
}
/**
* Insert or update this entity depending on its state.
*
@@ -198,6 +198,18 @@ public final class OrderBy<T> implements Serializable {
return list.hashCode();
}
/**
* Clear the orderBy removing any current order by properties.
* <p>
* This is intended to be used when some code creates a query with a
* 'default' order by clause and some other code may clear the 'default'
* order by clause and replace.
* </p>
*/
public void clear() {
list.clear();
}
/**
* A property and its ascending descending order.
*/
@@ -0,0 +1,17 @@
package com.avaje.ebean.annotation;
/**
* Specify the DB storage type used to with <code>@DbEnumValue</code>.
*/
public enum DbEnumType {
/**
* Store values as database INTEGER.
*/
INTEGER,
/**
* Store values as database VARCHAR.
*/
VARCHAR
}
@@ -0,0 +1,44 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Specify a method on an Enum that returns the value that should be stored in the DB.
* <p>
* This is the preferred option for mapping Enum's to DB values (preferred over the JPA
* standard @Enumerated and Ebean's @EnumValue annotations).
* </p>
* <h3>Example:</h3>
* <pre>{@code
*
* public enum Status {
* NEW("N"),
* ACTIVE("A"),
* INACTIVE("I");
*
* String dbValue;
* Status(String dbValue) {
* this.dbValue = dbValue;
* }
*
* @DbEnumValue
* public String getValue() {
* return dbValue;
* }
* }
*
* }</pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DbEnumValue {
/**
* Specify the database type used to store the values (VARCHAR or INTEGER).
*/
DbEnumType storage() default DbEnumType.VARCHAR;
}
@@ -25,7 +25,7 @@ import java.lang.annotation.Target;
* }</pre>
* <p>
* This is an alternative to using the JPA standard approach or Ebean's
* {@link EnumMapping} annotation.
* {@link DbEnumValue} annotation.
* </p>
* <p>
* Note that if all the EnumValue values are parsable as Integers then Ebean
@@ -449,7 +449,18 @@ public final class EntityBeanIntercept implements Serializable {
public int getPropertyLength() {
return owner._ebean_getPropertyNames().length;
}
/**
* Set the loaded state of the property given it's name.
*/
public void setPropertyLoaded(String propertyName, boolean loaded) {
int position = findProperty(propertyName);
if (position == -1) {
throw new IllegalArgumentException("Property "+propertyName+" not found");
}
loadedProps[position] = loaded;
}
/**
* Set the property to be treated as unloaded. Used for properties initialised in default
* constructor.
@@ -45,13 +45,29 @@ public class DbConstraintNaming {
protected String ckPrefix = "ck_";
protected String ckSuffix = "";
protected boolean lowerCaseNames = true;
protected MaxLength maxLength;
protected DbConstraintNormalise normalise = new DbConstraintNormalise();
protected DbConstraintNormalise normalise;
/**
* Construct using default of lower case for both table and column names.
*/
public DbConstraintNaming() {
this(true, true);
}
/**
* Construct specifying if lower case should be used (for both table and column names).
*/
public DbConstraintNaming(boolean lowerCase) {
this(lowerCase, lowerCase);
}
/**
* Construct specifying if lower case should be used for both table and column names.
*/
public DbConstraintNaming(boolean lowerCaseTableNames, boolean lowerCaseColumnNames) {
this.normalise = new DbConstraintNormalise(lowerCaseTableNames, lowerCaseColumnNames);
}
/**
@@ -167,7 +183,6 @@ public class DbConstraintNaming {
* quoted identifier characters (",',[,] etc).
*/
public String normaliseTable(String tableName) {
return normalise.normaliseTable(tableName);
}
@@ -175,18 +190,20 @@ public class DbConstraintNaming {
* Normalise the column name by removing any quoted identifier characters (",',[,] etc).
*/
public String normaliseColumn(String tableName) {
return normalise.normaliseColumn(tableName);
}
/**
* Lower case the table or column name checking for quoted identifiers.
* Lower case the table name checking for quoted identifiers.
*/
public String lowerName(String tableName) {
if (lowerCaseNames && normalise.notQuoted(tableName)) {
return tableName.toLowerCase();
}
return tableName;
public String lowerTableName(String tableName) {
return normalise.lowerTableName(tableName);
}
/**
* Lower case the column name checking for quoted identifiers.
*/
public String lowerColumnName(String name) {
return normalise.lowerColumnName(name);
}
}
@@ -8,11 +8,17 @@ public class DbConstraintNormalise {
protected final String[] quotedIdentifiers;
protected boolean lowerCaseTables = true;
protected final boolean lowerCaseTables;
protected boolean lowerCaseColumns = true;
protected final boolean lowerCaseColumns;
public DbConstraintNormalise() {
this(true, true);
}
public DbConstraintNormalise(boolean lowerCaseTables, boolean lowerCaseColumns) {
this.lowerCaseTables = lowerCaseTables;
this.lowerCaseColumns = lowerCaseColumns;
this.quotedIdentifiers = new String[]{"\"", "'", "[", "]", "`"};
}
@@ -46,6 +52,26 @@ public class DbConstraintNormalise {
return columnName;
}
/**
* Lower case the table name checking for quoted identifiers.
*/
public String lowerTableName(String tableName) {
if (lowerCaseTables && notQuoted(tableName)) {
return tableName.toLowerCase();
}
return tableName;
}
/**
* Lower case the column name checking for quoted identifiers.
*/
public String lowerColumnName(String name) {
if (lowerCaseColumns && notQuoted(name)) {
return name.toLowerCase();
}
return name;
}
/**
* Trim off the platform quoted identifier quotes like [ ' and ".
*/
@@ -18,6 +18,14 @@ public final class PropertyMap implements Serializable {
private final LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
/**
* Return properties loaded from test-ebean.properties.
*/
public static Properties testProperties() {
PropertyMap propertyMap = PropertyMapLoader.loadTestProperties();
return (propertyMap == null) ? new Properties() : propertyMap.asProperties();
}
public static Properties defaultProperties() {
PropertyMap propertyMap = PropertyMapLoader.loadGlobalProperties();
return (propertyMap == null) ? new Properties() : propertyMap.asProperties();
@@ -14,17 +14,34 @@ final class PropertyMapLoader {
private static final Logger logger = LoggerFactory.getLogger(PropertyMapLoader.class);
/**
* Load the <code>test-ebean.properties</code>.
*/
public static PropertyMap loadTestProperties() {
return load(null, "test-ebean.properties", false);
}
/**
* Load the ebean.properties (and test-ebean.properties if present).
*/
public static PropertyMap loadGlobalProperties() {
boolean loadTestProperties = false;
String fileName = System.getenv("EBEAN_PROPS_FILE");
if (fileName == null) {
fileName = System.getProperty("ebean.props.file");
if (fileName == null) {
loadTestProperties = true;
fileName = "ebean.properties";
}
}
return load(null, fileName);
PropertyMap map = load(null, fileName, true);
if (loadTestProperties) {
// load test properties if present in classpath
load(map, "test-ebean.properties", false);
}
return map;
}
/**
@@ -35,11 +52,13 @@ final class PropertyMapLoader {
* @param fileName
* the name of the properties file to load.
*/
public static PropertyMap load(PropertyMap p, String fileName) {
public static PropertyMap load(PropertyMap p, String fileName, boolean errorOnNull) {
InputStream is = findInputStream(fileName);
if (is == null) {
logger.error(fileName + " not found");
if (errorOnNull) {
logger.error(fileName + " not found");
}
return p;
} else {
return load(p, is);
@@ -234,7 +234,12 @@ public class ServerConfig {
* Indicates that Ebean should use autoCommit friendly Transactions and TransactionManager.
*/
private boolean autoCommitMode;
/**
* Set to true if transaction begin should be started with explicit statement.
*/
private boolean explicitTransactionBeginMode;
/**
* The data source JNDI name if using a JNDI DataSource.
*/
@@ -276,9 +281,9 @@ public class ServerConfig {
private boolean updatesDeleteMissingChildren = true;
/**
* Setting to indicate if UUID should be stored as binary(16) or varchar(40).
* Setting to indicate if UUID should be stored as binary(16) or varchar(40) or native DB type (for H2 and Postgres).
*/
private boolean uuidStoreAsBinary;
private DbUuid dbUuid = DbUuid.AUTO;
private List<BeanFindController> findControllers = new ArrayList<BeanFindController>();
@@ -1220,6 +1225,28 @@ public class ServerConfig {
this.autoCommitMode = autoCommitMode;
}
/**
* Return true if transaction begin should be started with explicit statement.
*/
public boolean isExplicitTransactionBeginMode() {
return explicitTransactionBeginMode;
}
/**
* Set to true if transaction begin should be started with explicit statement.
* <p>
* This works for H2 and Postgres but not for Oracle - only use this if you first name
* is Daryl or you have explicitly talked to Rob about this feature.
* </p>
* <p>
* This is generally not expected to be turned on but instead allow transactions to start
* implicitly which is generally the standard approach.
* </p>
*/
public void setExplicitTransactionBeginMode(boolean explicitTransactionBeginMode) {
this.explicitTransactionBeginMode = explicitTransactionBeginMode;
}
/**
* Return a value used to represent TRUE in the database.
* <p>
@@ -1439,19 +1466,18 @@ public class ServerConfig {
this.dbEncrypt = dbEncrypt;
}
/**
* Return true if UUID should be stored as binary(16) (as opposed to varchar(40)).
* Return the DB type used to store UUID.
*/
public boolean isUuidStoreAsBinary() {
return uuidStoreAsBinary;
public DbUuid getDbUuid() {
return dbUuid;
}
/**
* Set to true if UUID should be stored as binary(16) (as opposed to varchar(40)).
* Set the DB type used to store UUID.
*/
public void setUuidStoreAsBinary(boolean uuidStoreAsBinary) {
this.uuidStoreAsBinary = uuidStoreAsBinary;
public void setDbUuid(DbUuid dbUuid) {
this.dbUuid = dbUuid;
}
/**
@@ -2014,6 +2040,21 @@ public class ServerConfig {
loadSettings(p);
}
/**
* Load settings from test-ebean.properties and do nothing if the properties is not found.
* <p>
* This is typically used when test-ebean.properties is put into the test class path and used
* to configure Ebean for running tests.
* </p>
*/
public void loadTestProperties() {
Properties properties = PropertyMap.testProperties();
if (!properties.isEmpty()) {
PropertiesWrapper p = new PropertiesWrapper("ebean", name, properties);
loadSettings(p);
}
}
/**
* Return the properties that we used for configuration and were set via a call to loadFromProperties().
*/
@@ -2022,8 +2063,11 @@ public class ServerConfig {
}
@SuppressWarnings("unchecked")
private <T> T createInstance(PropertiesWrapper p, Class<T> pluginType, String key) {
private <T> T createInstance(PropertiesWrapper p, Class<T> pluginType, String key, T instance) {
if (instance != null) {
return instance;
}
String classname = p.get(key, null);
return classname == null ? null : (T) ClassUtil.newInstance(classname);
}
@@ -2067,18 +2111,19 @@ public class ServerConfig {
}
loadDataSourceSettings(p);
explicitTransactionBeginMode = p.getBoolean("explicitTransactionBeginMode", explicitTransactionBeginMode);
autoCommitMode = p.getBoolean("autoCommitMode", autoCommitMode);
useJtaTransactionManager = p.getBoolean("useJtaTransactionManager", useJtaTransactionManager);
currentUserProvider = createInstance(p, CurrentUserProvider.class, "currentUserProvider");
disableClasspathSearch = p.getBoolean("disableClasspathSearch", disableClasspathSearch);
databasePlatform = createInstance(p, DatabasePlatform.class, "databasePlatform");
encryptKeyManager = createInstance(p, EncryptKeyManager.class, "encryptKeyManager");
encryptDeployManager = createInstance(p, EncryptDeployManager.class, "encryptDeployManager");
encryptor = createInstance(p, Encryptor.class, "encryptor");
dbEncrypt = createInstance(p, DbEncrypt.class, "dbEncrypt");
serverCacheFactory = createInstance(p, ServerCacheFactory.class, "serverCacheFactory");
serverCacheManager = createInstance(p, ServerCacheManager.class, "serverCacheManager");
currentUserProvider = createInstance(p, CurrentUserProvider.class, "currentUserProvider", currentUserProvider);
databasePlatform = createInstance(p, DatabasePlatform.class, "databasePlatform", databasePlatform);
encryptKeyManager = createInstance(p, EncryptKeyManager.class, "encryptKeyManager", encryptKeyManager);
encryptDeployManager = createInstance(p, EncryptDeployManager.class, "encryptDeployManager", encryptDeployManager);
encryptor = createInstance(p, Encryptor.class, "encryptor", encryptor);
dbEncrypt = createInstance(p, DbEncrypt.class, "dbEncrypt", dbEncrypt);
serverCacheFactory = createInstance(p, ServerCacheFactory.class, "serverCacheFactory", serverCacheFactory);
serverCacheManager = createInstance(p, ServerCacheManager.class, "serverCacheManager", serverCacheManager);
cacheWarmingDelay = p.getInt("cacheWarmingDelay", cacheWarmingDelay);
classPathReaderClassName = p.get("classpathreader");
@@ -2124,7 +2169,10 @@ public class ServerConfig {
databaseBooleanTrue = p.get("databaseBooleanTrue", databaseBooleanTrue);
databaseBooleanFalse = p.get("databaseBooleanFalse", databaseBooleanFalse);
databasePlatformName = p.get("databasePlatformName", databasePlatformName);
uuidStoreAsBinary = p.getBoolean("uuidStoreAsBinary", uuidStoreAsBinary);
dbUuid = p.getEnum(DbUuid.class, "dbuuid", dbUuid);
if (p.getBoolean("uuidStoreAsBinary", false)) {
dbUuid = DbUuid.BINARY;
}
localTimeWithNanos = p.getBoolean("localTimeWithNanos", localTimeWithNanos);
lazyLoadBatchSize = p.getInt("lazyLoadBatchSize", lazyLoadBatchSize);
@@ -2146,7 +2194,7 @@ public class ServerConfig {
private NamingConvention createNamingConvention(PropertiesWrapper properties, NamingConvention namingConvention) {
NamingConvention nc = createInstance(properties, NamingConvention.class, "namingconvention");
NamingConvention nc = createInstance(properties, NamingConvention.class, "namingconvention", null);
return (nc != null) ? nc : namingConvention;
}
@@ -2246,4 +2294,25 @@ public class ServerConfig {
public void setExpressionEqualsWithNullAsNoop(boolean expressionEqualsWithNullAsNoop) {
this.expressionEqualsWithNullAsNoop = expressionEqualsWithNullAsNoop;
}
/**
* Specify how UUID is stored.
*/
public enum DbUuid {
/**
* Store using native UUID in H2 and Postgres.
*/
AUTO,
/**
* Store using DB VARCHAR.
*/
VARCHAR,
/**
* Store using DB BINARY.
*/
BINARY
}
}
@@ -1,18 +1,16 @@
package com.avaje.ebean.config.dbplatform;
import java.sql.Types;
import javax.sql.DataSource;
import com.avaje.ebean.BackgroundExecutor;
import com.avaje.ebean.Query;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlHandler;
import com.avaje.ebean.dbmigration.ddlgeneration.platform.PlatformDdl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.sql.Types;
/**
* Database platform specific settings.
*/
@@ -54,28 +52,33 @@ public class DatabasePlatform {
*/
protected OnQueryOnly onQueryOnly = OnQueryOnly.ROLLBACK;
/**
* The open quote used by quoted identifiers.
/**
* The open quote used by quoted identifiers.
*/
protected String openQuote = "\"";
/**
* The close quote used by quoted identifiers.
* The close quote used by quoted identifiers.
*/
protected String closeQuote = "\"";
/**
* For limit/offset, row_number etc limiting of SQL queries.
/**
* For limit/offset, row_number etc limiting of SQL queries.
*/
protected SqlLimiter sqlLimiter = new LimitOffsetSqlLimiter();
/**
* Mapping of JDBC to Database types.
/**
* Mapping of JDBC to Database types.
*/
protected DbTypeMap dbTypeMap = new DbTypeMap();
/**
* Defines DB identity/sequence features.
* Set to true if the DB has native UUID type support.
*/
protected boolean nativeUuidType;
/**
* Defines DB identity/sequence features.
*/
protected DbIdentity dbIdentity = new DbIdentity();
@@ -84,28 +87,28 @@ public class DatabasePlatform {
*/
protected DbHistorySupport historySupport;
/**
* The JDBC type to map booleans to (by default).
/**
* The JDBC type to map booleans to (by default).
*/
protected int booleanDbType = Types.BOOLEAN;
/**
* The JDBC type to map Blob to.
/**
* The JDBC type to map Blob to.
*/
protected int blobDbType = Types.BLOB;
/**
* The JDBC type to map Clob to.
/**
* The JDBC type to map Clob to.
*/
protected int clobDbType = Types.CLOB;
/**
* For Oracle treat empty strings as null.
/**
* For Oracle treat empty strings as null.
*/
protected boolean treatEmptyStringsAsNull;
/**
* The database platform name.
/**
* The database platform name.
*/
protected String name = "generic";
@@ -124,7 +127,7 @@ public class DatabasePlatform {
* The like clause. Can be overridden to disable default escape character.
*/
protected String likeClause = "like ?";
protected DbEncrypt dbEncrypt;
protected boolean idInExpandedForm;
@@ -219,16 +222,12 @@ public class DatabasePlatform {
/**
* Return a DB Sequence based IdGenerator.
*
* @param be
* the BackgroundExecutor that can be used to load the sequence if
* desired
* @param ds
* the DataSource
* @param seqName
* the name of the sequence
* @param batchSize
* the number of sequences that should be loaded
*
* @param be the BackgroundExecutor that can be used to load the sequence if
* desired
* @param ds the DataSource
* @param seqName the name of the sequence
* @param batchSize the number of sequences that should be loaded
*/
public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, int batchSize) {
return null;
@@ -276,9 +275,16 @@ public class DatabasePlatform {
this.historySupport = historySupport;
}
/**
* Return true if the DB supports native UUID.
*/
public boolean isNativeUuidType() {
return nativeUuidType;
}
/**
* Return the mapping of JDBC to DB types.
*
*
* @return the db type map
*/
public DbTypeMap getDbTypeMap() {
@@ -315,7 +321,7 @@ public class DatabasePlatform {
/**
* Return the close quote for quoted identifiers.
*
*
* @return the close quote
*/
public String getCloseQuote() {
@@ -324,7 +330,7 @@ public class DatabasePlatform {
/**
* Return the open quote for quoted identifiers.
*
*
* @return the open quote
*/
public String getOpenQuote() {
@@ -333,7 +339,7 @@ public class DatabasePlatform {
/**
* Return the JDBC type used to store booleans.
*
*
* @return the boolean db type
*/
public int getBooleanDbType() {
@@ -363,7 +369,7 @@ public class DatabasePlatform {
/**
* Return true if empty strings should be treated as null.
*
*
* @return true, if checks if is treat empty strings as null
*/
public boolean isTreatEmptyStringsAsNull() {
@@ -399,7 +405,7 @@ public class DatabasePlatform {
/**
* Return the DB identity/sequence features for this platform.
*
*
* @return the db identity
*/
public DbIdentity getDbIdentity() {
@@ -412,7 +418,7 @@ public class DatabasePlatform {
* <p>
* Basically add the clauses for limit/offset, rownum, row_number().
* </p>
*
*
* @return the sql limiter
*/
public SqlLimiter getSqlLimiter() {
@@ -421,15 +427,12 @@ public class DatabasePlatform {
/**
* Convert backticks to the platform specific open quote and close quote
*
* <p>
* Specific plugins may implement this method to cater for platform specific
* naming rules.
* </p>
*
* @param dbName
* the db name
*
*
* @param dbName the db name
* @return the string
*/
public String convertQuotedIdentifiers(String dbName) {
@@ -8,6 +8,11 @@ package com.avaje.ebean.config.dbplatform;
*/
public class DbType {
/**
* DB native UUID type (H2 and Postgres).
*/
public static final int UUID = 5010;
/**
* Type to map Map content to Postgres HSTORE.
*/
@@ -49,6 +49,8 @@ public class DbTypeMap {
lookup.put("TIME", Types.TIME);
lookup.put("TIMESTAMP", Types.TIMESTAMP);
lookup.put("UUID", DbType.UUID);
// Not standard java.sql.Types
// logical JSON storage types
lookup.put("JSON", DbType.JSON);
@@ -101,6 +103,9 @@ public class DbTypeMap {
put(Types.BLOB, new DbType("blob"));
put(Types.CLOB, new DbType("clob"));
// DB native UUID support (H2 and Postgres)
put(DbType.UUID, new DbType("uuid"));
if (logicalTypes) {
// keep it logical for 2 layer DDL generation
put(DbType.HSTORE, new DbType("hstore"));
@@ -55,7 +55,7 @@ public abstract class DbViewHistorySupport implements DbHistorySupport {
public String getAsOfPredicate(String asOfTableAlias, String asOfSysPeriod) {
// (sys_period_start < ? and (sys_period_end is null or sys_period_end > ?));
return "(" + asOfTableAlias + "." + asOfSysPeriod + "_start" + " < ? and (" + asOfTableAlias + "." + asOfSysPeriod + "_end" + " is null or " + asOfTableAlias + "." + asOfSysPeriod + "_end" + " > ?))";
return "(" + asOfTableAlias + "." + asOfSysPeriod + "_start" + " <= ? and (" + asOfTableAlias + "." + asOfSysPeriod + "_end" + " is null or " + asOfTableAlias + "." + asOfSysPeriod + "_end" + " > ?))";
}
/**
@@ -0,0 +1,8 @@
package com.avaje.ebean.config.dbplatform;
/**
* Runtime support for @History with H2.
*/
public class H2HistorySupport extends DbViewHistorySupport {
}
@@ -0,0 +1,127 @@
package com.avaje.ebean.config.dbplatform;
import org.h2.api.Trigger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* H2 database trigger used to populate history tables to support the @History feature.
*/
public class H2HistoryTrigger implements Trigger {
private static final Logger logger = LoggerFactory.getLogger(H2HistoryTrigger.class);
/**
* Hardcoding the column and history table suffix for now. Not sure how to get that
* configuration into the trigger instance nicely as it is instantiated by H2.
*/
private static final String SYS_PERIOD_START = "SYS_PERIOD_START";
private static final String SYS_PERIOD_END = "SYS_PERIOD_END";
private static final String HISTORY_SUFFIX = "_history";
/**
* SQL to insert into the history table.
*/
private String insertHistorySql;
/**
* Position of SYS_PERIOD_START column in the Object[].
*/
private int effectStartPosition;
/**
* Position of SYS_PERIOD_END column in the Object[].
*/
private int effectEndPosition;
@Override
public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException {
// get the columns for the table
ResultSet rs = conn.getMetaData().getColumns(null, schemaName, tableName, null);
// build the insert into history table SQL
StringBuilder insertSql = new StringBuilder(150);
insertSql.append("insert into ").append(tableName).append(HISTORY_SUFFIX).append(" (");
int count = 0;
List<String> columns = new ArrayList<String>();
while (rs.next()) {
if (++count > 1) {
insertSql.append(",");
}
String columnName = rs.getString("COLUMN_NAME");
if (columnName.equalsIgnoreCase(SYS_PERIOD_START)) {
this.effectStartPosition = count - 1;
} else if (columnName.equalsIgnoreCase(SYS_PERIOD_END)) {
this.effectEndPosition = count - 1;
}
insertSql.append(columnName);
columns.add(columnName);
}
insertSql.append(") values (");
for (int i = 0; i < count; i++) {
if (i > 0) {
insertSql.append(",");
}
insertSql.append("?");
}
insertSql.append(");");
this.insertHistorySql = insertSql.toString();
logger.debug("History table insert sql: {}", insertHistorySql);
}
@Override
public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException {
if (oldRow != null) {
// a delete or update event
Timestamp now = new Timestamp(System.currentTimeMillis());
oldRow[effectEndPosition] = now;
if (newRow != null) {
// update event. Set the effective start timestamp to now.
newRow[effectStartPosition] = now;
}
if (logger.isDebugEnabled()) {
logger.debug("History insert: {}", Arrays.toString(oldRow));
}
insertIntoHistory(connection, oldRow);
}
}
/**
* Insert the data into the history table.
*/
private void insertIntoHistory(Connection connection, Object[] oldRow) throws SQLException {
PreparedStatement stmt = connection.prepareStatement(insertHistorySql);
try {
for (int i = 0; i < oldRow.length; i++) {
stmt.setObject(i + 1, oldRow[i]);
}
stmt.executeUpdate();
} finally {
stmt.close();
}
}
@Override
public void close() throws SQLException {
}
@Override
public void remove() throws SQLException {
}
}
@@ -15,6 +15,8 @@ public class H2Platform extends DatabasePlatform {
this.name = "h2";
this.dbEncrypt = new H2DbEncrypt();
this.platformDdl = new H2Ddl(this.dbTypeMap, dbIdentity);
this.historySupport = new H2HistorySupport();
this.nativeUuidType = true;
// only support getGeneratedKeys with non-batch JDBC
// so generally use SEQUENCE instead of IDENTITY for H2
@@ -5,5 +5,4 @@ package com.avaje.ebean.config.dbplatform;
*/
public class MySqlHistorySupport extends DbViewHistorySupport {
}
@@ -23,10 +23,10 @@ public class PostgresPlatform extends DatabasePlatform {
// OnQueryOnly.CLOSE as a performance optimisation on Postgres
this.onQueryOnly = OnQueryOnly.CLOSE;
this.likeClause = "like ? escape''";
this.selectCountWithAlias = true;
this.blobDbType = Types.LONGVARBINARY;
this.clobDbType = Types.VARCHAR;
this.nativeUuidType = true;
this.dbEncrypt = new PostgresDbEncrypt();
this.historySupport = new PostgresHistorySupport();
@@ -263,6 +263,74 @@ public class DdlGenerator implements SpiEbeanPlugin {
}
}
/**
* Local utility used to detect the end of statements / separate statements.
* This is often just the semicolon character but for trigger/procedures this
* detects the $$ demarcation used in the history DDL generation for MySql and
* Postgres.
*/
static class StatementsSeparator {
ArrayList<String> statements = new ArrayList<String>();
boolean trimDelimiter;
boolean inDbProcedure;
StringBuilder sb = new StringBuilder();
void lineContainsDollars(String line) {
if (inDbProcedure) {
if (trimDelimiter) {
line = line.replace("$$","");
}
endOfStatement(line);
} else {
// MySql style delimiter needs to be trimmed/removed
trimDelimiter = line.equals("delimiter $$");
if (!trimDelimiter) {
sb.append(line).append(" ");
}
}
inDbProcedure = !inDbProcedure;
}
void endOfStatement(String line) {
// end of Db procedure
sb.append(line);
statements.add(sb.toString().trim());
sb = new StringBuilder();
}
void nextLine(String line) {
if (line.contains("$$")) {
lineContainsDollars(line);
return;
}
if (inDbProcedure) {
sb.append(line).append(" ");
return;
}
int semiPos = line.indexOf(';');
if (semiPos == -1) {
sb.append(line).append(" ");
} else if (semiPos == line.length() - 1) {
// semicolon at end of line
endOfStatement(line);
} else {
// semicolon in middle of line
String preSemi = line.substring(0, semiPos);
endOfStatement(preSemi);
sb.append(line.substring(semiPos + 1));
}
}
}
/**
* Break up the sql in reader into a list of statements using the semi-colon
* character;
@@ -271,35 +339,16 @@ public class DdlGenerator implements SpiEbeanPlugin {
try {
BufferedReader br = new BufferedReader(reader);
StatementsSeparator statements = new StatementsSeparator();
ArrayList<String> statements = new ArrayList<String>();
StringBuilder sb = new StringBuilder();
String s;
while ((s = br.readLine()) != null) {
s = s.trim();
int semiPos = s.indexOf(';');
if (semiPos == -1) {
sb.append(s).append(" ");
} else if (semiPos == s.length() - 1) {
// semicolon at end of line
sb.append(s);
statements.add(sb.toString().trim());
sb = new StringBuilder();
} else {
// semicolon in middle of line
String preSemi = s.substring(0, semiPos);
sb.append(preSemi);
statements.add(sb.toString().trim());
sb = new StringBuilder();
sb.append(s.substring(semiPos + 1));
}
statements.nextLine(s);
}
return statements;
return statements.statements;
} catch (IOException e) {
throw new PersistenceException(e);
}
@@ -98,7 +98,7 @@ public class BaseTableDdl implements TableDdl {
reset();
String tableName = lowerName(createTable.getName());
String tableName = lowerTableName(createTable.getName());
List<Column> columns = createTable.getColumn();
List<Column> pk = determinePrimaryKeyColumns(columns);
@@ -248,7 +248,7 @@ public class BaseTableDdl implements TableDdl {
protected void writeForeignKey(DdlWrite write, String fkName, String tableName, String[] columns, String refTable, String[] refColumns, String indexName) throws IOException {
tableName = lowerName(tableName);
tableName = lowerTableName(tableName);
DdlBuffer fkeyBuffer = write.applyForeignKeys();
alterTableAddForeignKey(fkeyBuffer, fkName, tableName, columns, refTable, refColumns);
@@ -282,7 +282,7 @@ public class BaseTableDdl implements TableDdl {
if (i > 0) {
buffer.append(",");
}
buffer.append(lowerName(columns[i].trim()));
buffer.append(lowerColumnName(columns[i].trim()));
}
buffer.append(")");
}
@@ -367,7 +367,7 @@ public class BaseTableDdl implements TableDdl {
buffer.append(",").newLine();
buffer.append(" constraint ").append(uqName).append(" unique ");
buffer.append("(");
buffer.append(lowerName(column.getName()));
buffer.append(lowerColumnName(column.getName()));
buffer.append(")");
}
@@ -401,14 +401,17 @@ public class BaseTableDdl implements TableDdl {
}
/**
* Convert the table or column name to lower case.
* <p>
* This is passed up to the platformDdl to override as desired.
* Generally lower case with underscore is a good cross database
* choice for column/table names.
* Convert the table lower case.
*/
protected String lowerName(String name) {
return naming.lowerName(name);
protected String lowerTableName(String name) {
return naming.lowerTableName(name);
}
/**
* Convert the column name to lower case.
*/
protected String lowerColumnName(String name) {
return naming.lowerColumnName(name);
}
/**
@@ -176,7 +176,7 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
String platformType = platformDdl.convert(type, false);
buffer.append(" ");
buffer.append(platformDdl.lowerName(columnName), 29);
buffer.append(platformDdl.lowerColumnName(columnName), 29);
buffer.append(platformType);
}
@@ -10,6 +10,7 @@ public class H2Ddl extends PlatformDdl {
public H2Ddl(DbTypeMap platformTypes, DbIdentity dbIdentity) {
super(platformTypes, dbIdentity);
this.historyDdl = new H2HistoryDdl();
}
}
@@ -0,0 +1,73 @@
package com.avaje.ebean.dbmigration.ddlgeneration.platform;
import com.avaje.ebean.config.dbplatform.H2HistoryTrigger;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
import com.avaje.ebean.dbmigration.model.MTable;
import java.io.IOException;
import java.util.List;
/**
* H2 history support using DB triggers to maintain a history table.
*/
public class H2HistoryDdl extends DbTriggerBasedHistoryDdl {
private static final String TRIGGER_CLASS = H2HistoryTrigger.class.getName();
public H2HistoryDdl() {
}
@Override
protected void dropTriggers(DdlBuffer buffer, String baseTable) throws IOException {
buffer.append("drop trigger ").append(updateTriggerName(baseTable)).endOfStatement();
}
@Override
protected void createTriggers(DdlWrite writer, MTable table) throws IOException {
String baseTableName = table.getName();
String historyTableName = historyTableName(baseTableName);
List<String> includedColumns = includedColumnNames(table);
DdlBuffer apply = writer.applyHistory();
addCreateTrigger(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
}
@Override
protected void regenerateHistoryTriggers(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
String baseTableName = table.getName();
String historyTableName = historyTableName(baseTableName);
List<String> includedColumns = includedColumnNames(table);
DdlBuffer apply = writer.applyHistory();
apply.append("-- Regenerated ").newLine();
apply.append("-- changes: ").append(update.description()).newLine();
dropTriggers(apply, baseTableName);
addCreateTrigger(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
// put a reverted version into the rollback buffer
update.toRevertedColumns(includedColumns);
DdlBuffer rollback = writer.rollback();
rollback.append("-- Revert regenerated ").newLine();
rollback.append("-- revert changes: ").append(update.description()).newLine();
dropTriggers(rollback, baseTableName);
addCreateTrigger(rollback, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
}
private void addCreateTrigger(DdlBuffer apply, String triggerName, String baseTable, String historyTable, List<String> includedColumns) throws IOException {
// Note that this does not take into account the historyTable name (excepts _history suffix) and
// does not take into account excluded columns (all columns included in history)
apply
.append("create trigger ").append(triggerName).append(" before update,delete on ").append(baseTable)
.append(" for each row call \"" + TRIGGER_CLASS + "\";").newLine();
}
}
@@ -136,7 +136,7 @@ public class PlatformDdl {
String platformType = convert(column.getType(), identityColumn);
buffer.append(" ");
buffer.append(lowerName(column.getName()), 29);
buffer.append(lowerColumnName(column.getName()), 29);
buffer.append(platformType);
if (isTrue(column.isNotnull()) || isTrue(column.isPrimaryKey())) {
buffer.append(" not null");
@@ -254,7 +254,7 @@ public class PlatformDdl {
appendColumns(columns, buffer);
buffer
.append(" references ")
.append(lowerName(refTable));
.append(lowerTableName(refTable));
appendColumns(refColumns, buffer);
appendWithSpace(foreignKeyRestrict, buffer);
@@ -346,7 +346,7 @@ public class PlatformDdl {
if (i > 0) {
buffer.append(",");
}
buffer.append(lowerName(columns[i].trim()));
buffer.append(lowerColumnName(columns[i].trim()));
}
buffer.append(")");
}
@@ -358,16 +358,26 @@ public class PlatformDdl {
}
/**
* Convert the table or column name to lower case.
* Convert the table to lower case.
* <p>
* This is passed up to the platformDdl to override as desired.
* Generally lower case with underscore is a good cross database
* Override as desired. Generally lower case with underscore is a good cross database
* choice for column/table names.
*/
protected String lowerName(String name) {
return naming.lowerName(name);
protected String lowerTableName(String name) {
return naming.lowerTableName(name);
}
/**
* Convert the column name to lower case.
* <p>
* Override as desired. Generally lower case with underscore is a good cross database
* choice for column/table names.
*/
protected String lowerColumnName(String name) {
return naming.lowerColumnName(name);
}
/**
* Null safe Boolean true test.
*/
@@ -40,7 +40,7 @@ public class PostgresHistoryDdl extends DbTriggerBasedHistoryDdl {
.endOfStatement();
if (whenCreatedColumn != null) {
apply.append("update table ").append(baseTableName).append(" set ")
apply.append("update ").append(baseTableName).append(" set ")
.append(sysPeriod).append(" = tstzrange(").append(whenCreatedColumn).append(", null)").endOfStatement();
}
}
@@ -81,12 +81,12 @@ public class PostgresHistoryDdl extends DbTriggerBasedHistoryDdl {
appendInsertIntoHistory(apply, historyTable, includedColumns);
apply
.append(" NEW.").append(sysPeriod).append(" = tstzrange(CURRENT_TIMESTAMP,null);").newLine()
.append(" return new;").newLine().newLine();
.append(" return new;").newLine();
apply
.append(" elsif (TG_OP = 'DELETE') then").newLine();
appendInsertIntoHistory(apply, historyTable, includedColumns);
apply
.append(" return old;").newLine().newLine();
.append(" return old;").newLine();
apply
.append(" end if;").newLine()
.append("end;").newLine()
@@ -14,7 +14,7 @@ import java.util.List;
*/
public class MCompoundForeignKey {
private final String name;
private String name;
private final String referenceTable;
private final List<String> columns = new ArrayList<String>();
private final List<String> referenceColumns = new ArrayList<String>();
@@ -47,6 +47,28 @@ public class MCompoundForeignKey {
return fk;
}
/**
* Add a counter to the foreign key and index names to avoid duplication.
*/
public void addNameSuffix(int counter) {
this.name = name + "_" + counter;
this.indexName = indexName + "_" + counter;
}
/**
* Return the foreign key name.
*/
public String getName() {
return name;
}
/**
* Return the index name.
*/
public String getIndexName() {
return indexName;
}
/**
* Return the columns making up the foreign key in order.
*/
@@ -14,6 +14,7 @@ import com.avaje.ebean.dbmigration.migration.UniqueConstraint;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -482,4 +483,32 @@ public class MTable {
return (value == 0) ? null : BigInteger.valueOf(value);
}
/**
* Check if there are duplicate foreign keys.
* <p>
* This can occur when an ManyToMany relates back to itself.
* </p>
*/
public void checkDuplicateForeignKeys() {
if (hasDuplicateForeignKeys()) {
int counter = 1;
for (MCompoundForeignKey fk : compoundKeys) {
fk.addNameSuffix(counter++);
}
}
}
/**
* Return true if the foreign key names are not unique.
*/
private boolean hasDuplicateForeignKeys() {
Set<String> fkNames = new HashSet<String>();
for (MCompoundForeignKey fk : compoundKeys) {
if (!fkNames.add(fk.getName())) {
return true;
}
}
return false;
}
}
@@ -213,8 +213,8 @@ public class ModelContainer {
/**
* Add a table (typically from reading EbeanServer meta data).
*/
public void addTable(MTable table) {
tables.put(table.getName(), table);
public MTable addTable(MTable table) {
return tables.put(table.getName(), table);
}
/**
@@ -67,8 +67,8 @@ public class ModelBuildContext {
return maxLength(constraintNaming.checkConstraintName(tableName, columnName), checkCount);
}
public void addTable(MTable table) {
model.addTable(table);
public MTable addTable(MTable table) {
return model.addTable(table);
}
public void addIndex(String indexName, String tableName, String columnName) {
@@ -35,7 +35,11 @@ public class ModelBuildIntersectionTable {
public void build() {
intersectionTable = createTable();
ctx.addTable(intersectionTable);
MTable existingTable = ctx.addTable(intersectionTable);
if (existingTable != null) {
throw new IllegalStateException("Property " + manyProp.getFullBeanName() + " has duplicate ManyToMany intersection table " + intersectionTable.getName()
+ ". Please use @JoinTable to define unique table to use");
}
buildFkConstraints();
}
@@ -47,6 +51,8 @@ public class ModelBuildIntersectionTable {
BeanDescriptor<?> targetDesc = manyProp.getTargetDescriptor();
buildFkConstraints(targetDesc, tableJoin.columns(), false);
intersectionTable.checkDuplicateForeignKeys();
}
@@ -0,0 +1,8 @@
/**
* Generation DDL migration scripts based on changes to the model.
* <p>
* You can use <code>DbMigration</code> to compare the entity bean model to its prior state
* and generate DDL for the differences - adding tables, columns etc.
* </p>
*/
package com.avaje.ebean.dbmigration;
@@ -0,0 +1,13 @@
/**
* Provides a built in change log mechanism and can audit changes.
* <p>
* Although you can build something similar using existing event adapters such as <code>BeanPersistController</code>
* this is built for purpose to reduce the effort and provide optimal performance for auditing or logging changes.
* </p>
* <p>
* By default you can annotate beans with <code>@ChangeLog</code> and associated change events are logged by default
* in a JSON form with appropriate auditing attributes such as who made the changes and ip address of the user etc
* via implementation of <code>ChangeLogPrepare</code>
* </p>
*/
package com.avaje.ebean.event.changelog;
@@ -0,0 +1,8 @@
/**
* Provides Auditing of read events including queries and L2 cache.
* <p>
* Provides a built support for supplied an audit of all the 'read events' for beans annotated
* with <code>@ReadAudit</code>
* </p>
*/
package com.avaje.ebean.event.readaudit;
@@ -0,0 +1,8 @@
/**
* Provides a API for plugins.
* <p>
* Plugins can get and read meta data about the entity beans and enhance or utilise the
* functionality of the EbeanServer.
* </p>
*/
package com.avaje.ebean.plugin;
@@ -59,11 +59,6 @@ public class ScopedTransaction implements SpiTransaction {
}
}
@Override
public void endQueryOnly() {
transaction.endQueryOnly();
}
@Override
public String getLogPrefix() {
return transaction.getLogPrefix();
@@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.api;
import java.util.ArrayList;
import com.avaje.ebeaninternal.server.core.JsonExpressionHandler;
import com.avaje.ebeaninternal.server.core.SpiOrmQueryRequest;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
@@ -10,6 +11,11 @@ import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
*/
public interface SpiExpressionRequest {
/**
* Return the DB specific JSON expression handler.
*/
JsonExpressionHandler getJsonHander();
/**
* Parse the logical property name to the deployment name.
*/
@@ -19,11 +19,6 @@ import com.avaje.ebeaninternal.server.persist.BatchControl;
*/
public interface SpiTransaction extends Transaction {
/**
* End the transaction when had query only use.
*/
void endQueryOnly();
/**
* Return the string prefix with the transactin id and label used in logging.
*/
@@ -1,14 +1,14 @@
package com.avaje.ebeaninternal.server.core;
import java.beans.PropertyChangeListener;
import java.util.Map;
import java.util.Set;
import com.avaje.ebean.BeanState;
import com.avaje.ebean.ValuePair;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.bean.EntityBeanIntercept;
import java.beans.PropertyChangeListener;
import java.util.Map;
import java.util.Set;
/**
* Default implementation of BeanState.
*/
@@ -23,9 +23,13 @@ public class DefaultBeanState implements BeanState {
this.intercept = entityBean._ebean_getIntercept();
}
public boolean isReference() {
return intercept.isReference();
}
public void setPropertyLoaded(String propertyName, boolean loaded) {
intercept.setPropertyLoaded(propertyName, loaded);
}
public boolean isReference() {
return intercept.isReference();
}
public boolean isNew() {
return intercept.isNew();
@@ -1301,6 +1301,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
SpiOrmQueryRequest<T> request = createQueryRequest(Type.DELETE, query, t);
try {
request.initTransIfRequired();
request.markNotQueryOnly();
return request.delete();
} finally {
request.endTransIfRequired();
@@ -39,6 +39,7 @@ import com.avaje.ebeaninternal.server.readaudit.DefaultReadAuditPrepare;
import com.avaje.ebeaninternal.server.text.json.DJsonContext;
import com.avaje.ebeaninternal.server.transaction.AutoCommitTransactionManager;
import com.avaje.ebeaninternal.server.transaction.DefaultTransactionScopeManager;
import com.avaje.ebeaninternal.server.transaction.ExplicitTransactionManager;
import com.avaje.ebeaninternal.server.transaction.ExternalTransactionScopeManager;
import com.avaje.ebeaninternal.server.transaction.JtaTransactionManager;
import com.avaje.ebeaninternal.server.transaction.TransactionManager;
@@ -208,11 +209,29 @@ public class InternalConfiguration {
* For 'As Of' queries return the number of bind variables per predicate.
*/
private Binder getBinder(TypeManager typeManager, DatabasePlatform databasePlatform) {
JsonExpressionHandler jsonHandler = getJsonExpressionHandler(databasePlatform);
DbHistorySupport historySupport = databasePlatform.getHistorySupport();
if (historySupport == null) {
return new Binder(typeManager, 0, false);
return new Binder(typeManager, 0, false, jsonHandler);
}
return new Binder(typeManager, historySupport.getBindCount(), historySupport.isBindWithFromClause());
return new Binder(typeManager, historySupport.getBindCount(), historySupport.isBindWithFromClause(), jsonHandler);
}
/**
* Return the JSON expression handler for the given database platform.
*/
private JsonExpressionHandler getJsonExpressionHandler(DatabasePlatform databasePlatform) {
String name = databasePlatform.getName();
if ("postgres".equalsIgnoreCase(name)) {
return new PostgresJsonExpression();
}
if ("oracle".equalsIgnoreCase(name)) {
return new OracleJsonExpression();
}
return new NotSupportedJsonExpression();
}
/**
@@ -220,6 +239,10 @@ public class InternalConfiguration {
*/
private TransactionManager createTransactionManager() {
if (serverConfig.isExplicitTransactionBeginMode()) {
return new ExplicitTransactionManager(clusterManager, backgroundExecutor, serverConfig, beanDescriptorManager, this.getBootupClasses());
}
if (isAutoCommitMode()) {
return new AutoCommitTransactionManager(clusterManager, backgroundExecutor, serverConfig, beanDescriptorManager, this.getBootupClasses());
}
@@ -0,0 +1,17 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Adds the db platform specific json expression.
*/
public interface JsonExpressionHandler {
/**
* Write the db platform specific json expression.
*/
void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value);
}
@@ -0,0 +1,15 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Not supported JSON expression handler.
*/
public class NotSupportedJsonExpression implements JsonExpressionHandler {
@Override
public void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
throw new RuntimeException("JSON expressions only supported on Postgres and Oracle");
}
}
@@ -0,0 +1,24 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Postgres JSON expression handler
*/
public class OracleJsonExpression implements JsonExpressionHandler {
@Override
public void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
if (operator == Op.EXISTS) {
request.append("json_exists(").append(propName).append(", '$.").append(path).append("')");
} else if (operator == Op.NOT_EXISTS) {
request.append("not json_exists(").append(propName).append(", '$.").append(path).append("')");
} else {
request.append("json_value(").append(propName).append(", '$.").append(path).append("')");
request.append(operator.bind());
}
}
}
@@ -10,6 +10,7 @@ import javax.persistence.PersistenceException;
import com.avaje.ebean.*;
import com.avaje.ebean.bean.BeanCollection;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.bean.PersistenceContext;
import com.avaje.ebean.event.BeanFindController;
import com.avaje.ebean.event.BeanQueryRequest;
@@ -71,6 +72,13 @@ public final class OrmQueryRequest<T> extends BeanRequest implements BeanQueryRe
this.readOnly = query.isReadOnly();
}
/**
* Mark the transaction as not being query only.
*/
@Override
public void markNotQueryOnly() {
transaction.markNotQueryOnly();
}
/**
* Return the database platform like clause.
@@ -151,6 +159,14 @@ public final class OrmQueryRequest<T> extends BeanRequest implements BeanQueryRe
return persistenceContext;
}
/**
* Add the bean to the persistence context.
*/
public void persistenceContextAdd(EntityBean bean) {
Object id = beanDescriptor.getId(bean);
persistenceContext.put(id, bean);
}
/**
* This will create a local (readOnly) transaction if no current transaction
* exists.
@@ -210,7 +226,7 @@ public final class OrmQueryRequest<T> extends BeanRequest implements BeanQueryRe
*/
public void endTransIfRequired() {
if (createdTransaction) {
transaction.endQueryOnly();
transaction.commit();
}
}
@@ -0,0 +1,71 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Postgres JSON expression handler
*/
public class PostgresJsonExpression implements JsonExpressionHandler {
@Override
public void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
StringBuilder sb = new StringBuilder(50);
String[] paths = path.split("\\.");
if (paths.length == 1) {
// (t0.content ->> 'title') = 'Some value'
sb.append("(").append(propName).append(" ->> '").append(path).append("')");
} else {
// (t0.content #>> '{path,inner}') = 'Some value'
sb.append("(").append(propName).append(" #>> '{");
for (int i = 0; i < paths.length; i++) {
if (i > 0) {
sb.append(",");
}
sb.append(paths[i]);
}
sb.append("}')");
}
request.append(castType(sb.toString(), value));
request.append(operator.bind());
}
/**
* Postgres CAST the type if necessary as text values always returned from the json operators used.
*/
private String castType(String expression, Object value) {
if (value == null) {
// for exists and not-exists expressions
return expression;
}
// Postgres cast of returned text value
if (isIntegerType(value)) {
return expression+"::INTEGER";
}
if (isNumberType(value)) {
return expression+"::DECIMAL";
}
if (isBooleanType(value)) {
return expression+"::BOOLEAN";
}
return expression;
}
private boolean isBooleanType(Object value) {
return (value instanceof Boolean);
}
private boolean isIntegerType(Object value) {
return (value instanceof Integer) || (value instanceof Long);
}
private boolean isNumberType(Object value) {
return (value instanceof Number);
}
}
@@ -59,7 +59,7 @@ public final class RelationalQueryRequest {
*/
public void endTransIfRequired() {
if (createdTransaction) {
trans.endQueryOnly();
trans.commit();
}
}
@@ -112,4 +112,8 @@ public interface SpiOrmQueryRequest<T> {
*/
String getDBLikeClause();
/**
* Mark the underlying transaction as not being query only.
*/
void markNotQueryOnly();
}
@@ -73,7 +73,31 @@ public class DeployBeanPropertyLists {
this.propertyMap = new LinkedHashMap<String, BeanProperty>();
// see if there is a discriminator property we should add
String discriminatorColumn = null;
BeanProperty discProperty = null;
InheritInfo inheritInfo = deploy.getInheritInfo();
if (inheritInfo != null) {
// Create a BeanProperty for the discriminator column to support
// using RawSql queries with inheritance
discriminatorColumn = inheritInfo.getDiscriminatorColumn();
DeployBeanProperty discDeployProp = new DeployBeanProperty(deploy, String.class, new ScalarTypeString(), null);
discDeployProp.setDiscriminator();
discDeployProp.setName(discriminatorColumn);
discDeployProp.setDbColumn(discriminatorColumn);
// only register it in the propertyMap. This might not be used if
// an explicit property is mapped to the discriminator on the bean
discProperty = new BeanProperty(desc, discDeployProp);
}
for (DeployBeanProperty prop : deploy.propertiesAll()) {
if (discriminatorColumn != null && discriminatorColumn.equals(prop.getDbColumn())) {
// we have an explicit property mapped to the discriminator column
prop.setDiscriminator();
discProperty = null;
}
BeanProperty beanProp = createBeanProperty(owner, prop);
propertyMap.put(beanProp.getName(), beanProp);
}
@@ -84,21 +108,12 @@ public class DeployBeanPropertyLists {
allocateToList(prop);
}
InheritInfo inheritInfo = deploy.getInheritInfo();
if (inheritInfo != null) {
// Create a BeanProperty for the discriminator column to support
// using RawSql queries with inheritance
String discriminatorColumn = inheritInfo.getDiscriminatorColumn();
DeployBeanProperty discDeployProp = new DeployBeanProperty(deploy, String.class, new ScalarTypeString(), null);
discDeployProp.setDiscriminator();
discDeployProp.setName(discriminatorColumn);
discDeployProp.setDbColumn(discriminatorColumn);
// create the discriminator BeanProperty and only register it in the propertyMap
BeanProperty dprop = new BeanProperty(desc, discDeployProp);
propertyMap.put(dprop.getName(), dprop);
if (discProperty != null) {
// put the discriminator property into the property map only
// (after the real properties have been organised into their lists)
propertyMap.put(discProperty.getName(), discProperty);
}
List<DeployTableJoin> deployTableJoins = deploy.getTableJoins();
tableJoins = new TableJoin[deployTableJoins.size()];
for (int i = 0; i < deployTableJoins.size(); i++) {
@@ -76,6 +76,8 @@ public class AnnotationFields extends AnnotationParser {
*/
private void readAssocOne(DeployBeanProperty prop) {
readJsonAnnotations(prop);
Id id = get(prop, Id.class);
if (id != null) {
prop.setId();
@@ -117,25 +119,7 @@ public class AnnotationFields extends AnnotationParser {
readColumn(column, prop);
}
if (jacksonAnnotationsPresent) {
com.fasterxml.jackson.annotation.JsonIgnore jsonIgnore = get(prop, com.fasterxml.jackson.annotation.JsonIgnore.class);
if (jsonIgnore != null) {
prop.setJsonSerialize(!jsonIgnore.value());
prop.setJsonDeserialize(!jsonIgnore.value());
}
}
Expose expose = get(prop, Expose.class);
if (expose != null) {
prop.setJsonSerialize(expose.serialize());
prop.setJsonDeserialize(expose.deserialize());
}
JsonIgnore jsonIgnore = get(prop, JsonIgnore.class);
if (jsonIgnore != null) {
prop.setJsonSerialize(jsonIgnore.serialize());
prop.setJsonDeserialize(jsonIgnore.deserialize());
}
readJsonAnnotations(prop);
if (prop.getDbColumn() == null) {
// No @Column annotation or @Column.name() not set
@@ -291,6 +275,28 @@ public class AnnotationFields extends AnnotationParser {
}
}
private void readJsonAnnotations(DeployBeanProperty prop) {
if (jacksonAnnotationsPresent) {
com.fasterxml.jackson.annotation.JsonIgnore jsonIgnore = get(prop, com.fasterxml.jackson.annotation.JsonIgnore.class);
if (jsonIgnore != null) {
prop.setJsonSerialize(!jsonIgnore.value());
prop.setJsonDeserialize(!jsonIgnore.value());
}
}
Expose expose = get(prop, Expose.class);
if (expose != null) {
prop.setJsonSerialize(expose.serialize());
prop.setJsonDeserialize(expose.deserialize());
}
JsonIgnore jsonIgnore = get(prop, JsonIgnore.class);
if (jsonIgnore != null) {
prop.setJsonSerialize(jsonIgnore.serialize());
prop.setJsonDeserialize(jsonIgnore.deserialize());
}
}
private boolean hasRelationshipItem(DeployBeanProperty prop) {
return get(prop, OneToMany.class) != null ||
get(prop, ManyToOne.class) != null ||
@@ -111,8 +111,8 @@ public class DeployUtil {
}
ScalarType<?> scalarType = typeManager.getScalarType(enumType);
if (scalarType == null) {
// see if it has a Mapping in avaje.properties
scalarType = typeManager.createEnumScalarType(enumType);
// look for @DbEnumValue or @EnumValue annotations etc
scalarType = typeManager.createEnumScalarType((Class<? extends Enum<?>>)enumType);
if (scalarType == null) {
// use JPA normal Enum type (without mapping)
EnumType type = enumerated != null ? enumerated.value() : null;
@@ -211,7 +211,7 @@ public class DefaultExampleExpression implements SpiExpression, ExampleExpressio
// exclude the zero values typically to weed out
// primitive int and long that initialise to 0
if (includeZeros || !isZero(value)) {
list.add(new SimpleExpression(propName, SimpleExpression.Op.EQ, value));
list.add(new SimpleExpression(propName, Op.EQ, value));
}
}
}
@@ -1,9 +1,5 @@
package com.avaje.ebeaninternal.server.expression;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.avaje.ebean.ExampleExpression;
import com.avaje.ebean.Expression;
import com.avaje.ebean.ExpressionFactory;
@@ -15,6 +11,10 @@ import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebeaninternal.api.SpiExpressionFactory;
import com.avaje.ebeaninternal.api.SpiQuery;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Default Expression factory for creating standard expressions.
*/
@@ -36,6 +36,42 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
return "sql";
}
public Expression jsonExists(String propertyName, String path) {
return new JsonPathExpression(propertyName, path, Op.EXISTS, null);
}
public Expression jsonNotExists(String propertyName, String path) {
return new JsonPathExpression(propertyName, path, Op.NOT_EXISTS, null);
}
public Expression jsonEqualTo(String propertyName, String path, Object value) {
return new JsonPathExpression(propertyName, path, Op.EQ, value);
}
public Expression jsonNotEqualTo(String propertyName, String path, Object value) {
return new JsonPathExpression(propertyName, path, Op.NOT_EQ, value);
}
public Expression jsonGreaterThan(String propertyName, String path, Object value) {
return new JsonPathExpression(propertyName, path, Op.GT, value);
}
public Expression jsonGreaterOrEqual(String propertyName, String path, Object value) {
return new JsonPathExpression(propertyName, path, Op.GT_EQ, value);
}
public Expression jsonLessThan(String propertyName, String path, Object value) {
return new JsonPathExpression(propertyName, path, Op.LT, value);
}
public Expression jsonLessOrEqualTo(String propertyName, String path, Object value) {
return new JsonPathExpression(propertyName, path, Op.LT_EQ, value);
}
public Expression jsonBetween(String propertyName, String path, Object lowerValue, Object upperValue) {
return new JsonPathExpression(propertyName, path, lowerValue, upperValue);
}
/**
* Equal To - property equal to the given value.
*/
@@ -43,7 +79,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
if (value == null) {
return equalsWithNullAsNoop ? NoopExpression.INSTANCE : isNull(propertyName);
}
return new SimpleExpression(propertyName, SimpleExpression.Op.EQ, value);
return new SimpleExpression(propertyName, Op.EQ, value);
}
/**
@@ -53,7 +89,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
if (value == null) {
return equalsWithNullAsNoop ? NoopExpression.INSTANCE : isNotNull(propertyName);
}
return new SimpleExpression(propertyName, SimpleExpression.Op.NOT_EQ, value);
return new SimpleExpression(propertyName, Op.NOT_EQ, value);
}
/**
@@ -88,7 +124,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
*/
public Expression gt(String propertyName, Object value) {
return new SimpleExpression(propertyName, SimpleExpression.Op.GT, value);
return new SimpleExpression(propertyName, Op.GT, value);
}
/**
@@ -97,7 +133,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
*/
public Expression ge(String propertyName, Object value) {
return new SimpleExpression(propertyName, SimpleExpression.Op.GT_EQ, value);
return new SimpleExpression(propertyName, Op.GT_EQ, value);
}
/**
@@ -105,7 +141,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
*/
public Expression lt(String propertyName, Object value) {
return new SimpleExpression(propertyName, SimpleExpression.Op.LT, value);
return new SimpleExpression(propertyName, Op.LT, value);
}
/**
@@ -113,7 +149,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
*/
public Expression le(String propertyName, Object value) {
return new SimpleExpression(propertyName, SimpleExpression.Op.LT_EQ, value);
return new SimpleExpression(propertyName, Op.LT_EQ, value);
}
/**
@@ -0,0 +1,96 @@
package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebean.event.BeanQueryRequest;
import com.avaje.ebeaninternal.api.HashQueryPlanBuilder;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
/**
* Generally speaking tests the value at a given path in the JSON document.
* <p>
* Supports the usual operators (equal to, greater than etc).
* </p>
* <p>
* The value passed in is expected to be a valid JSON type so string, number, boolean.
* </p>
*/
class JsonPathExpression extends AbstractExpression {
/**
* The path in the JSON document in dot notation form.
*/
protected final String path;
/**
* The expression operator.
*/
protected final Op operator;
/**
* The bind value used to compare against the document path value.
*/
protected final Object value;
/**
* For Between this is the upper bind value.
*/
protected final Object upperValue;
/**
* Construct for Operator (not BETWEEN though).
*/
JsonPathExpression(String propertyName, String path, Op operator, Object value) {
super(propertyName);
this.path = path;
this.operator = operator;
this.value = value;
this.upperValue = null;
}
/**
* Construct for BETWEEN expression.
*/
JsonPathExpression(String propertyName, String path, Object value, Object upperValue) {
super(propertyName);
this.path = path;
this.operator = Op.BETWEEN;
this.value = value;
this.upperValue = upperValue;
}
@Override
public void queryAutoTuneHash(HashQueryPlanBuilder builder) {
builder.add(JsonPathExpression.class).add(propName).add(path).add(operator);
}
@Override
public void queryPlanHash(BeanQueryRequest<?> request, HashQueryPlanBuilder builder) {
queryAutoTuneHash(builder);
}
@Override
public int queryBindHash() {
int hc = (value == null) ? 0 : value.hashCode();
hc = (upperValue == null) ? hc : hc * 31 + upperValue.hashCode();
return hc;
}
@Override
public void addSql(SpiExpressionRequest request) {
// Use DB specific expression handling (Postgres and Oracle supported)
request.getJsonHander().addSql(request, propName, path, operator, value);
}
@Override
public void addBindValues(SpiExpressionRequest request) {
if (value != null) {
// value is null for EXISTS/NOT EXISTS
request.addBindValue(value);
}
if (upperValue != null) {
// upperValue only for BETWEEN operator
request.addBindValue(upperValue);
}
}
}
@@ -313,6 +313,78 @@ abstract class JunctionExpression<T> implements Junction<T>, SpiExpression, Expr
return exprList.findUnique();
}
/**
* Path exists - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonExists(String propertyName, String path) {
return exprList.jsonExists(propertyName, path);
}
/**
* Path does not exist - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonNotExists(String propertyName, String path){
return exprList.jsonNotExists(propertyName, path);
}
/**
* Equal to - for the value at the given path in the JSON document.
*/
@Override
public ExpressionList<T> jsonEqualTo(String propertyName, String path, Object value){
return exprList.jsonEqualTo(propertyName, path, value);
}
/**
* Not Equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonNotEqualTo(String propertyName, String path, Object val){
return exprList.jsonNotEqualTo(propertyName, path, val);
}
/**
* Greater than - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonGreaterThan(String propertyName, String path, Object val){
return exprList.jsonGreaterThan(propertyName, path, val);
}
/**
* Greater than or equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonGreaterOrEqual(String propertyName, String path, Object val){
return exprList.jsonGreaterOrEqual(propertyName, path, val);
}
/**
* Less than - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonLessThan(String propertyName, String path, Object val){
return exprList.jsonLessThan(propertyName, path, val);
}
/**
* Less than or equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonLessOrEqualTo(String propertyName, String path, Object val){
return exprList.jsonLessOrEqualTo(propertyName, path, val);
}
/**
* Between - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonBetween(String propertyName, String path, Object lowerValue, Object upperValue){
return exprList.jsonBetween(propertyName, path, lowerValue, upperValue);
}
@Override
public ExpressionList<T> ge(String propertyName, Object value) {
return exprList.ge(propertyName, value);
@@ -0,0 +1,67 @@
package com.avaje.ebeaninternal.server.expression;
/**
* Simple operators - equals, greater than, less than etc.
*/
public enum Op {
/**
* Exists (JSON).
*/
EXISTS(" is not null "),
/**
* Not Exists (JSON).
*/
NOT_EXISTS(" is null "),
/**
* Between (JSON).
*/
BETWEEN(" between ? and ? "),
/**
* Equal to
*/
EQ(" = ? "),
/**
* Not equal to.
*/
NOT_EQ(" <> ? "),
/**
*
* Less than.
*/
LT(" < ? "),
/**
* Less than or equal to.
*/
LT_EQ(" <= ? "),
/**
* Greater than.
*/
GT(" > ? "),
/**
* Greater than or equal to.
*/
GT_EQ(" >= ? ");
final String exp;
Op(String exp) {
this.exp = exp;
}
/**
* Return the bind expression include JDBC ? bind placeholder.
*/
public String bind() {
return exp;
}
}
@@ -10,20 +10,6 @@ public class SimpleExpression extends AbstractExpression {
private static final long serialVersionUID = -382881395755603790L;
enum Op {
EQ(" = ? "), NOT_EQ(" <> ? "), LT(" < ? "), LT_EQ(" <= ? "), GT(" > ? "), GT_EQ(" >= ? ");
final String exp;
Op(String exp) {
this.exp = exp;
}
public String bind() {
return exp;
}
}
private final Op type;
private final Object value;
@@ -64,12 +64,16 @@ public final class ShutdownManager {
/**
* Deregister the Shutdown hook.
* <p>
* In calling this method it is expected that application code will invoke
* the shutdown() method.
* </p>
* <p>
* For running in a Servlet Container a redeploy will cause a shutdown, and
* for that case we need to make sure the shutdown hook is deregistered.
* </p>
*/
protected static void deregisterShutdownHook() {
public static void deregisterShutdownHook() {
synchronized (servers) {
try {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
@@ -87,7 +91,10 @@ public final class ShutdownManager {
protected static void registerShutdownHook() {
synchronized (servers) {
try {
Runtime.getRuntime().addShutdownHook(shutdownHook);
String value = System.getProperty("ebean.registerShutdownHook");
if (value == null || !value.trim().equalsIgnoreCase("false")) {
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
} catch (IllegalStateException ex) {
if (!ex.getMessage().equals("Shutdown in progress")) {
throw ex;
@@ -9,7 +9,9 @@ import java.util.List;
import javax.persistence.PersistenceException;
import com.avaje.ebean.config.dbplatform.DbType;
import com.avaje.ebeaninternal.api.BindParams;
import com.avaje.ebeaninternal.server.core.JsonExpressionHandler;
import com.avaje.ebeaninternal.server.core.Message;
import com.avaje.ebeaninternal.server.type.DataBind;
import com.avaje.ebeaninternal.server.type.ScalarType;
@@ -30,13 +32,16 @@ public class Binder {
private final boolean bindAsOfWithFromClause;
private final JsonExpressionHandler jsonExpressionHandler;
/**
* Set the PreparedStatement with which to bind variables to.
*/
public Binder(TypeManager typeManager, int asOfBindCount, boolean bindAsOfWithFromClause) {
public Binder(TypeManager typeManager, int asOfBindCount, boolean bindAsOfWithFromClause, JsonExpressionHandler jsonExpressionHandler) {
this.typeManager = typeManager;
this.asOfBindCount = asOfBindCount;
this.bindAsOfWithFromClause = bindAsOfWithFromClause;
this.jsonExpressionHandler = jsonExpressionHandler;
}
/**
@@ -303,6 +308,11 @@ public class Binder {
b.setBytes((byte[]) data);
break;
case DbType.UUID:
// native UUID support in H2 and Postgres
b.setObject(data);
break;
case java.sql.Types.OTHER:
b.setObject(data);
break;
@@ -375,4 +385,11 @@ public class Binder {
return false;
}
}
/**
* Return the JsonExpressionHandler specific to the database.
*/
public JsonExpressionHandler getJsonExpressionHandler() {
return jsonExpressionHandler;
}
}
@@ -643,6 +643,15 @@ public final class DefaultPersister implements Persister {
private boolean isCascade() {
return cascade;
}
public void modifyListenReset(BeanCollection<?> c) {
if (insertedParent) {
// after insert set the modify listening mode
// for private owned etc
c.setModifyListening(many.getModifyListenMode());
}
c.modifyReset();
}
}
private void saveMany(SaveManyPropRequest saveMany, boolean insertMode) {
@@ -684,6 +693,7 @@ public final class DefaultPersister implements Persister {
BeanCollection<?> c = (BeanCollection<?>) details;
Set<?> modifyRemovals = c.getModifyRemovals();
saveMany.modifyListenReset(c);
if (modifyRemovals != null && !modifyRemovals.isEmpty()) {
SpiTransaction t = saveMany.getTransaction();
@@ -908,6 +918,10 @@ public final class DefaultPersister implements Persister {
t.depth(+1);
if (additions != null && !additions.isEmpty()) {
// ensure any cascade batch has been flushed prior
// to inserting into the intersection table
t.flushBatch();
for (Object other : additions) {
EntityBean otherBean = (EntityBean) other;
// the object from the 'other' side of the ManyToMany
@@ -933,6 +947,10 @@ public final class DefaultPersister implements Persister {
}
}
if (deletions != null && !deletions.isEmpty()) {
// ensure any cascade batch has been flushed prior
// to inserting into the intersection table
t.flushBatch();
for (Object other : deletions) {
EntityBean otherDelete = (EntityBean) other;
// the object from the 'other' side of the ManyToMany
@@ -433,6 +433,7 @@ public class CQuery<T> implements DbReadContext, CancelableQuery {
} else {
// nextBean set to previously read currentBean
nextBean = currentBean;
request.persistenceContextAdd(nextBean);
// check the current row we have just moved to
if (checkForDifferentBean()) {
return true;
@@ -302,7 +302,7 @@ public class CQueryPredicates {
SpiExpressionList<?> whereExp = query.getWhereExpressions();
if (whereExp != null) {
DefaultExpressionRequest whereReq = new DefaultExpressionRequest(request, deployParser);
DefaultExpressionRequest whereReq = new DefaultExpressionRequest(request, deployParser, binder);
whereExprBindValues = whereExp.buildBindValues(whereReq);
if (buildSql) {
whereExprSql = whereExp.buildSql(whereReq);
@@ -314,7 +314,7 @@ public class CQueryPredicates {
OrmQueryProperties chunk = query.getDetail().getChunk(manyProperty.getName(), false);
SpiExpressionList<?> filterMany = chunk.getFilterMany();
if (filterMany != null) {
DefaultExpressionRequest filterReq = new DefaultExpressionRequest(request, deployParser);
DefaultExpressionRequest filterReq = new DefaultExpressionRequest(request, deployParser, binder);
filterManyExprBindValues = filterMany.buildBindValues(filterReq);
if (buildSql) {
filterManyExprSql = filterMany.buildSql(filterReq);
@@ -325,7 +325,7 @@ public class CQueryPredicates {
// having expression
SpiExpressionList<?> havingExpr = query.getHavingExpressions();
if (havingExpr != null) {
DefaultExpressionRequest havingReq = new DefaultExpressionRequest(request, deployParser);
DefaultExpressionRequest havingReq = new DefaultExpressionRequest(request, deployParser, binder);
havingExprBindValues = havingExpr.buildBindValues(havingReq);
if (buildSql) {
havingExprSql = havingExpr.buildSql(havingReq);
@@ -484,6 +484,7 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
copy.profilingListener = profilingListener;
copy.query = query;
copy.rootTableAlias = rootTableAlias;
copy.additionalWhere = additionalWhere;
copy.additionalHaving = additionalHaving;
copy.distinct = distinct;
@@ -0,0 +1,45 @@
package com.avaje.ebeaninternal.server.transaction;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* This only works for Postgres and H2 (and doesn't work for Oracle).
*
* Uses explicit begin statement to start the transactions.
*/
public class ExplicitJdbcTransaction extends JdbcTransaction {
public ExplicitJdbcTransaction(String id, boolean explicit, Connection connection, TransactionManager manager) {
super(id, explicit, connection, manager);
}
@Override
protected void checkAutoCommit(Connection connection) throws SQLException {
// begin the transaction explicitly
executeStatement("begin");
}
@Override
protected void performRollback() throws SQLException {
// Postgres needs this explicit rollback statement when used with AutoCommit=true
executeStatement("rollback");
}
@Override
protected void performCommit() throws SQLException {
// Postgres needs this explicit commit statement when used with AutoCommit=true
executeStatement("commit");
}
private void executeStatement(String statement) throws SQLException {
PreparedStatement stmt = connection.prepareStatement(statement);
try {
stmt.execute();
} finally {
stmt.close();
}
}
}
@@ -0,0 +1,32 @@
package com.avaje.ebeaninternal.server.transaction;
import com.avaje.ebean.BackgroundExecutor;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebeaninternal.api.SpiTransaction;
import com.avaje.ebeaninternal.server.cluster.ClusterManager;
import com.avaje.ebeaninternal.server.core.BootupClasses;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager;
import java.sql.Connection;
/**
* TransactionManager where the transactions start with explicit "begin" statement.
*/
public class ExplicitTransactionManager extends TransactionManager {
public ExplicitTransactionManager(ClusterManager clusterManager, BackgroundExecutor backgroundExecutor,
ServerConfig config, BeanDescriptorManager descMgr, BootupClasses bootupClasses) {
super(clusterManager, backgroundExecutor, config, descMgr, bootupClasses);
}
/**
* Create a ExplicitJdbcTransaction.
*/
@Override
protected SpiTransaction createTransaction(boolean explicit, Connection c, long id) {
return new ExplicitJdbcTransaction(prefix + id, explicit, c, this);
}
}
@@ -849,23 +849,6 @@ public class JdbcTransaction implements SpiTransaction {
connection.commit();
}
/**
* End the transaction on a query only request.
*/
@Override
public void endQueryOnly() {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
try {
connectionEndForQueryOnly();
} finally {
// these will not throw an exception
deactivate();
notifyQueryOnly();
}
}
/**
* Commit the transaction.
*/
@@ -1,5 +1,7 @@
package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.annotation.DbEnumType;
import com.avaje.ebean.annotation.DbEnumValue;
import com.avaje.ebean.annotation.EnumMapping;
import com.avaje.ebean.annotation.EnumValue;
import com.avaje.ebean.config.*;
@@ -21,6 +23,7 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
@@ -419,8 +422,10 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
return value;
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private boolean isIntegerType(String s) {
protected boolean isIntegerType(String s) {
if (isLeadingZeros(s)) {
return false;
}
try {
Integer.parseInt(s);
return true;
@@ -429,6 +434,13 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
}
}
/**
* Treat leading zeros as a non-integer for enum values.
*/
private boolean isLeadingZeros(String s) {
return s.length() > 1 && s.charAt(0) == '0';
}
/**
* Create the Mapping of Enum fields to DB values using EnumValue annotations.
* <p>
@@ -469,7 +481,16 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
* much shorter codes used in the DB.
* </p>
*/
public ScalarType<?> createEnumScalarType(Class<?> enumType) {
public ScalarType<?> createEnumScalarType(Class<? extends Enum<?>> enumType) {
Method[] methods = enumType.getMethods();
for (int i = 0; i <methods.length; i++) {
DbEnumValue dbValue = methods[i].getAnnotation(DbEnumValue.class);
if (dbValue != null) {
boolean integerValues = DbEnumType.INTEGER == dbValue.storage();
return createEnumScalarTypeDbValue(enumType, methods[i], integerValues);
}
}
// get the mapping information from EnumMapping
EnumMapping enumMapping = enumType.getAnnotation(EnumMapping.class);
@@ -487,6 +508,33 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
return createEnumScalarType(enumType, nameValueMap, integerType, dbColumnLength);
}
/**
* Create the Mapping of Enum fields to DB values using EnumValue annotations.
* <p>
* Return null if the EnumValue annotations are not present/used.
* </p>
*/
private ScalarType<?> createEnumScalarTypeDbValue(Class<? extends Enum<?>> enumType, Method method, boolean integerType) {
Map<String, String> nameValueMap = new HashMap<String, String>();
Enum<?>[] enumConstants = enumType.getEnumConstants();
for (int i = 0; i < enumConstants.length; i++) {
try {
Object value = method.invoke(enumConstants[i]);
nameValueMap.put(enumConstants[i].name(), value.toString());
} catch (Exception e) {
throw new IllegalArgumentException("Error trying to invoke DbEnumValue method on "+enumConstants[i], e);
}
}
if (nameValueMap.isEmpty()) {
// Not using EnumValue here
return null;
}
return createEnumScalarType(enumType, nameValueMap, integerType, 0);
}
/**
* Given the name value mapping and integer/string type and explicit DB column
* length create the ScalarType for the Enum.
@@ -768,7 +816,6 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
*/
protected void initialiseStandard(JsonConfig.DateTime mode, ServerConfig config) {
boolean binaryUUID = config.isUuidStoreAsBinary();
DatabasePlatform databasePlatform = config.getDatabasePlatform();
int platformClobType = databasePlatform.getClobDbType();
int platformBlobType = databasePlatform.getBlobDbType();
@@ -795,9 +842,17 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
nativeMap.put(Types.BIT, booleanType);
}
// Store UUID as binary(16) or varchar(40)
ScalarType<?> uuidType = (binaryUUID) ? new ScalarTypeUUIDBinary() : new ScalarTypeUUIDVarchar();
typeMap.put(UUID.class, uuidType);
boolean nativeUuidType = databasePlatform.isNativeUuidType();
ServerConfig.DbUuid dbUuid = config.getDbUuid();
if (nativeUuidType && dbUuid == ServerConfig.DbUuid.AUTO) {
// DB has native support for UUID
typeMap.put(UUID.class, new ScalarTypeUUIDNative());
} else {
// Store UUID as binary(16) or varchar(40)
ScalarType<?> uuidType = (ServerConfig.DbUuid.BINARY == dbUuid) ? new ScalarTypeUUIDBinary() : new ScalarTypeUUIDVarchar();
typeMap.put(UUID.class, uuidType);
}
typeMap.put(File.class, fileType);
typeMap.put(InetAddress.class, inetAddressType);
@@ -0,0 +1,120 @@
package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.config.dbplatform.DbType;
import com.avaje.ebean.text.json.JsonWriter;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.sql.SQLException;
import java.util.UUID;
/**
* Postgres Hstore type which maps Map<String,String> to a single 'HStore column' in the DB.
*/
@SuppressWarnings("rawtypes")
public class ScalarTypeUUIDNative extends ScalarTypeBase<UUID> {
public ScalarTypeUUIDNative() {
super(UUID.class, false, DbType.UUID);
}
@Override
public boolean isMutable() {
return false;
}
@Override
public boolean isDirty(Object value) {
return true;
}
@SuppressWarnings("unchecked")
@Override
public UUID read(DataReader dataReader) throws SQLException {
Object value = dataReader.getObject();
if (value == null) {
return null;
}
return (UUID)value;
}
@Override
public void bind(DataBind b, UUID value) throws SQLException {
b.setObject(value);
}
@Override
public Object toJdbcType(Object value) {
return value;
}
@Override
public UUID toBeanType(Object value) {
return (UUID) value;
}
@Override
public String formatValue(UUID v) {
return v.toString();
}
@Override
public UUID parse(String value) {
return UUID.fromString(value);
}
@Override
public UUID convertFromMillis(long dateTime) {
throw new RuntimeException("Should never be called");
}
@Override
public boolean isDateTimeCapable() {
return false;
}
@Override
public UUID readData(DataInput dataInput) throws IOException {
if (!dataInput.readBoolean()) {
return null;
} else {
String json = dataInput.readUTF();
return parse(json);
}
}
@Override
public void writeData(DataOutput dataOutput, UUID v) throws IOException {
if (v == null) {
dataOutput.writeBoolean(false);
} else {
dataOutput.writeBoolean(true);
String json = format(v);
dataOutput.writeUTF(json);
}
}
@Override
public void jsonWrite(JsonWriter writer, String name, UUID value) throws IOException {
// write the field name followed by the Map/JSON Object
if (value == null) {
writer.writeNullField(name);
} else {
writer.writeStringField(name, formatValue(value));
}
}
@Override
public UUID jsonRead(JsonParser parser, JsonToken event) throws IOException {
// at this point the BeanProperty has read the START_OBJECT token
// to check for a null value. Pass the START_OBJECT token through to
// the EJson parsing so that it knows the first token has been read
String strValue = parser.getValueAsString();
return strValue == null ? null : parse(strValue);
}
}
@@ -55,7 +55,7 @@ public interface TypeManager {
* Create a ScalarType for an Enum using a mapping (rather than JPA Ordinal
* or String which has limitations).
*/
ScalarType<?> createEnumScalarType(Class<?> enumType);
ScalarType<?> createEnumScalarType(Class<? extends Enum<?>> enumType);
/**
* Return the ScalarType used to handle JSON content.
@@ -4,13 +4,24 @@ import com.avaje.ebeaninternal.api.ClassPathSearchService;
import com.avaje.ebeaninternal.api.ClassUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.loader.jar.JarEntryData;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.*;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@@ -157,7 +168,7 @@ public class ClassPathSearch implements ClassPathSearchService {
} else if (element.isJarOrWar()) {
// search name including the ! offset if it is there
if (classPathSize == 1 || filter.isSearchJar(element.getJarNameWithOffset())) {
if (classPathSize == 1 || filter.isSearchJar(element.getJarNameWithOffset(), element.getJarOffset())) {
scanJar(element);
}
@@ -208,7 +219,7 @@ public class ClassPathSearch implements ClassPathSearchService {
private void scanDirectory(File directory) {
List<String> directoryFiles = getDirectoryFiles(directory);
searchFiles(Collections.enumeration(directoryFiles), null, null);
searchFiles(Collections.enumeration(directoryFiles), null, null, null);
}
private void scanUri(URI uri) throws IOException {
@@ -234,12 +245,14 @@ public class ClassPathSearch implements ClassPathSearchService {
File file = classPathEntry.classPath;
module = new JarFile(file);
logger.trace("scanJar file:{}", file);
List<URI> classPathFromManifest = getClassPathFromManifest(file, module.getManifest());
for (URI uri : classPathFromManifest) {
scanUri(uri);
}
searchFiles(module.entries(), classPathEntry.getJarName(), classPathEntry.jarOffset);
searchFiles(module.entries(), classPathEntry.getJarName(), classPathEntry.jarOffset, file);
} catch (MalformedURLException ex) {
throw new IOException("Bad classpath error: ", ex);
@@ -284,26 +297,30 @@ public class ClassPathSearch implements ClassPathSearchService {
/**
* Searches through the Java Archive (jar or war file) looking for classes
* that match our requirements.
*
* @param files - all of the files in the Java Archive, this is an enumeration
* @param entries - all of the entries in the Java Archive, this is an enumeration
* provided by the Jar file
* @param jarFileName - the name of the java archive
* @param jarOffset - an offset inside the archive to chop off the name of the class -
* this is used when we have bang path offsets (e.g.
* file:///myfile.war!/WEB-INF/classes)
* @param module the containing jar/war file (used for spring boot embedded jar scanning)
*/
private void searchFiles(Enumeration<?> files, String jarFileName, String jarOffset) {
private void searchFiles(Enumeration<?> entries, String jarFileName, String jarOffset, File module) {
if (files == null) {
if (entries == null) {
return;
}
/*
* Strips the first character off as all entries in a jar file have no /
* prefix. We want to come out with a name like WEB-INF/classes/ to ensure
* we filter the contents of the war/jar file by this.
*/
if (jarOffset != null) {
logger.debug("searchFiles jarFileName:{} jarOffset:{}", jarFileName, jarOffset);
// Strips the first character off as all entries in a jar file have no /
// prefix. We want to come out with a name like WEB-INF/classes/ to ensure
// we filter the contents of the war/jar file by this.
if ("/".equals(jarOffset)) {
// root level for runnable jar (spring boot etc)
jarOffset = null;
} else if (jarOffset != null) {
if (jarOffset.startsWith("/")) {
jarOffset = jarOffset.substring(1);
}
@@ -313,48 +330,90 @@ public class ClassPathSearch implements ClassPathSearchService {
}
}
while (files.hasMoreElements()) {
while (entries.hasMoreElements()) {
String fileName = files.nextElement().toString();
Object element = entries.nextElement();
String entryName = element.toString();
// we only want the class files
if (fileName.endsWith(".class") && (jarOffset == null || fileName.startsWith(jarOffset))) {
if (isEntryEmbeddedJar(module, entryName)) {
scanSpringBootEmbeddedJar(jarFileName, module, entryName);
}
if (jarOffset != null) {
// we got through here only if there is an offset and we
// matched it, so strip it off the file
// as we are trying to find the className
fileName = fileName.substring(jarOffset.length());
if (isEntryClass(jarOffset, entryName)) {
// check if it an 'interesting' class - entity etc
registerScannedClass(jarFileName, jarOffset, entryName);
}
}
}
/**
* Return true if this is an embedded jar that should be scanned.
*/
private boolean isEntryEmbeddedJar(File module, String entryName) {
return entryName.endsWith(".jar") && module != null && filter.isSearchJar(entryName, null);
}
/**
* Return true if this is a class that should be checked (for entity, interesting interface etc).
*/
private boolean isEntryClass(String jarOffset, String entryName) {
return entryName.endsWith(".class") && (jarOffset == null || entryName.startsWith(jarOffset));
}
private void scanSpringBootEmbeddedJar(String jarFileName, File module, String fileName) {
// spring boot embedded jar
logger.debug("spring boot embedded:{} : module:{}", fileName, module.getAbsoluteFile());
try {
org.springframework.boot.loader.jar.JarFile jarFile = new org.springframework.boot.loader.jar.JarFile(module);
org.springframework.boot.loader.jar.JarFile jarEntryFile = jarFile.getNestedJarFile(jarFile.getJarEntryData(fileName));
Iterator<JarEntryData> iterator = jarEntryFile.iterator();
while (iterator.hasNext()) {
JarEntryData jarEntryData = iterator.next();
if (jarEntryData.getName().toString().endsWith(".class")) {
logger.debug("... spring boot class entry:{}", jarEntryData.getName().toString());
registerScannedClass(jarFileName, null, jarEntryData.getName().toString());
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void registerScannedClass(String jarFileName, String jarOffset, String fileName) {
if (jarOffset != null) {
// we got through here only if there is an offset and we
// matched it, so strip it off the file
// as we are trying to find the className
fileName = fileName.substring(jarOffset.length());
}
String className = fileName.replace('/', '.').substring(0, fileName.length() - 6);
int lastPeriod = className.lastIndexOf(".");
String pckgName;
if (lastPeriod > 0) {
pckgName = className.substring(0, lastPeriod);
} else {
pckgName = "";
}
if (filter.isSearchPackage(pckgName)) {
// get the class for our class name
try {
Class<?> theClass = Class.forName(className, false, classLoader);
if (matcher.isMatch(theClass)) {
matchList.add(theClass);
registerHit(jarFileName, theClass);
}
String className = fileName.replace('/', '.').substring(0, fileName.length() - 6);
int lastPeriod = className.lastIndexOf(".");
String pckgName;
if (lastPeriod > 0) {
pckgName = className.substring(0, lastPeriod);
} else {
pckgName = "";
}
if (filter.isSearchPackage(pckgName)) {
// get the class for our class name
try {
Class<?> theClass = Class.forName(className, false, classLoader);
if (matcher.isMatch(theClass)) {
matchList.add(theClass);
registerHit(jarFileName, theClass);
}
} catch (ClassNotFoundException e) {
// expected to get this hence trace
logger.trace("Error searching classpath" + e.getMessage());
} catch (NoClassDefFoundError e) {
// expected to get this hence trace
logger.trace("Error searching classpath" + e.getMessage());
}
}
} catch (ClassNotFoundException e) {
// expected to get this hence trace
logger.trace("Error searching classpath" + e.getMessage());
} catch (NoClassDefFoundError e) {
// expected to get this hence trace
logger.trace("Error searching classpath" + e.getMessage());
}
}
}
@@ -498,6 +557,10 @@ public class ClassPathSearch implements ClassPathSearchService {
String getJarNameWithOffset() {
return (jarOffset == null) ? classPath.getName() : classPath.getName() + "!" + jarOffset;
}
String getJarOffset() {
return jarOffset;
}
}
}
@@ -122,17 +122,19 @@ public class ClassPathSearchFilter {
/**
* Return true if the jar should be included in the search.
*/
public boolean isSearchJar(String jarName) {
public boolean isSearchJar(String jarName, String jarOffset) {
if (containedIn(includeJarSet, jarName)) {
return true;
}
if (containedIn(includeJarSet, jarName)) {
return true;
}
if (containedIn(excludeJarSet, jarName)) {
return false;
}
return defaultJarMatch;
}
if (containedIn(excludeJarSet, jarName)) {
return false;
}
// by default scan the root level in runnable jar (spring boot etc)
return "/".equals(jarOffset) || defaultJarMatch;
}
/**
* Helper method to determine is a match is contained in the set.
@@ -363,6 +363,87 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
return hash;
}
/**
* Path exists - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonExists(String propertyName, String path) {
add(expr.jsonExists(propertyName, path));
return this;
}
/**
* Path does not exist - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonNotExists(String propertyName, String path){
add(expr.jsonNotExists(propertyName, path));
return this;
}
/**
* Equal to expression for the value at the given path in the JSON document.
*/
@Override
public ExpressionList<T> jsonEqualTo(String propertyName, String path, Object value){
add(expr.jsonEqualTo(propertyName, path, value));
return this;
}
/**
* Not Equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonNotEqualTo(String propertyName, String path, Object val){
add(expr.jsonNotEqualTo(propertyName, path, val));
return this;
}
/**
* Greater than - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonGreaterThan(String propertyName, String path, Object val){
add(expr.jsonGreaterThan(propertyName, path, val));
return this;
}
/**
* Greater than or equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonGreaterOrEqual(String propertyName, String path, Object val){
add(expr.jsonGreaterOrEqual(propertyName, path, val));
return this;
}
/**
* Less than - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonLessThan(String propertyName, String path, Object val){
add(expr.jsonLessThan(propertyName, path, val));
return this;
}
/**
* Less than or equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonLessOrEqualTo(String propertyName, String path, Object val){
add(expr.jsonLessOrEqualTo(propertyName, path, val));
return this;
}
/**
* Between - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonBetween(String propertyName, String path, Object lowerValue, Object upperValue){
add(expr.jsonBetween(propertyName, path, lowerValue, upperValue));
return this;
}
@Override
public ExpressionList<T> eq(String propertyName, Object value) {
add(expr.eq(propertyName, value));
@@ -3,9 +3,11 @@ package com.avaje.ebeaninternal.util;
import java.util.ArrayList;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.core.JsonExpressionHandler;
import com.avaje.ebeaninternal.server.core.SpiOrmQueryRequest;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.DeployParser;
import com.avaje.ebeaninternal.server.persist.Binder;
public class DefaultExpressionRequest implements SpiExpressionRequest {
@@ -19,18 +21,26 @@ public class DefaultExpressionRequest implements SpiExpressionRequest {
private final DeployParser deployParser;
private final Binder binder;
private int paramIndex;
public DefaultExpressionRequest(SpiOrmQueryRequest<?> queryRequest, DeployParser deployParser) {
public DefaultExpressionRequest(SpiOrmQueryRequest<?> queryRequest, DeployParser deployParser, Binder binder) {
this.queryRequest = queryRequest;
this.beanDescriptor = queryRequest.getBeanDescriptor();
this.deployParser = deployParser;
this.binder = binder;
}
public DefaultExpressionRequest(BeanDescriptor<?> beanDescriptor) {
this.beanDescriptor = beanDescriptor;
this.queryRequest = null;
this.deployParser = null;
this.binder = null;
}
public JsonExpressionHandler getJsonHander() {
return binder.getJsonExpressionHandler();
}
public String parseDeploy(String logicalProp) {
@@ -0,0 +1,98 @@
package com.avaje.ebean;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.tests.model.converstation.User;
import org.avaje.ebeantest.LoggedSqlCollector;
import org.junit.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class TestUnsetLoadedProperties extends BaseTestCase {
@Test
public void testUnsetPropertyLoadedState() {
// populate db with a user
User seed = new User();
seed.setName("someName");
seed.setEmail("some@junk.com");
seed.save();
// our bean to perform stateless update
User user = new User();
user.setId(seed.getId());
user.setName("name mod");
user.setEmail("change@junk.com");
BeanState beanState = Ebean.getBeanState(user);
assertThat(beanState.getLoadedProps()).containsExactly("id", "name", "email");
user.markPropertyUnset("email");
assertThat(beanState.getLoadedProps()).containsExactly("id", "name");
LoggedSqlCollector.start();
user.update();
List<String> loggedSql = LoggedSqlCollector.stop();
assertThat(loggedSql).hasSize(1);
assertThat(loggedSql.get(0)).doesNotContain("email");
}
@Test
public void testUnloadVia_EntityBeanIntercept_setPropertyLoaded() {
// our bean to perform stateless update
User user = new User();
user.setId(42L);
user.setName("name mod");
user.setEmail("change@junk.com");
BeanState beanState = Ebean.getBeanState(user);
assertThat(beanState.getLoadedProps()).containsExactly("id", "name", "email");
// unset the loaded state for email
((EntityBean)user)._ebean_getIntercept().setPropertyLoaded("email", false);
assertThat(beanState.getLoadedProps()).containsExactly("id", "name");
}
@Test
public void testUnloadVia_Model_markPropertyUnset() {
// our bean to perform stateless update
User user = new User();
user.setId(42L);
user.setName("name mod");
user.setEmail("change@junk.com");
BeanState beanState = Ebean.getBeanState(user);
assertThat(beanState.getLoadedProps()).containsExactly("id", "name", "email");
user.markPropertyUnset("email");
assertThat(beanState.getLoadedProps()).containsExactly("id", "name");
}
@Test
public void testUnloadVia_BeanState_setPropertyLoaded() {
// our bean to perform stateless update
User user = new User();
user.setId(42L);
user.setName("name mod");
user.setEmail("change@junk.com");
BeanState beanState = Ebean.getBeanState(user);
assertThat(beanState.getLoadedProps()).containsExactly("id", "name", "email");
Ebean.getBeanState(user).setPropertyLoaded("email", false);
assertThat(beanState.getLoadedProps()).containsExactly("id", "name");
}
}
@@ -34,4 +34,40 @@ public class DbConstraintNamingTest {
assertThat(naming.normaliseTable("foo_bar]")).isEqualTo("foo_bar");
}
@Test
public void testDefaultToLower() throws Exception {
assertThat(naming.normaliseTable("SCH.FOO_BAR]")).isEqualTo("foo_bar");
assertThat(naming.lowerTableName("SCH.FOO_BAR]")).isEqualTo("SCH.FOO_BAR]");
assertThat(naming.lowerTableName("SCH.FOO_BAR")).isEqualTo("sch.foo_bar");
assertThat(naming.lowerColumnName("SCH.FOO_BAR]")).isEqualTo("SCH.FOO_BAR]");
assertThat(naming.lowerColumnName("SCH.FOO_BAR")).isEqualTo("sch.foo_bar");
}
@Test
public void testNoLowerCaseTable() throws Exception {
DbConstraintNaming naming = new DbConstraintNaming(false, true);
assertThat(naming.normaliseTable("SCH.FOO_BAR]")).isEqualTo("FOO_BAR");
assertThat(naming.normaliseColumn("SCH.FOO_BAR]")).isEqualTo("sch.foo_bar");
assertThat(naming.lowerTableName("SCH.FOO_BAR]")).isEqualTo("SCH.FOO_BAR]");
// table name not lowered
assertThat(naming.lowerTableName("SCH.FOO_BAR")).isEqualTo("SCH.FOO_BAR");
assertThat(naming.lowerColumnName("SCH.FOO_BAR]")).isEqualTo("SCH.FOO_BAR]");
assertThat(naming.lowerColumnName("SCH.FOO_BAR")).isEqualTo("sch.foo_bar");
}
@Test
public void testNoLowerCaseColumn() throws Exception {
DbConstraintNaming naming = new DbConstraintNaming(true, false);
assertThat(naming.normaliseTable("SCH.FOO_BAR]")).isEqualTo("foo_bar");
assertThat(naming.normaliseColumn("SCH.FOO_BAR]")).isEqualTo("SCH.FOO_BAR");
assertThat(naming.lowerTableName("SCH.FOO_BAR]")).isEqualTo("SCH.FOO_BAR]");
assertThat(naming.lowerTableName("SCH.FOO_BAR")).isEqualTo("sch.foo_bar");
assertThat(naming.lowerColumnName("SCH.FOO_BAR]")).isEqualTo("SCH.FOO_BAR]");
// column name not lowered
assertThat(naming.lowerColumnName("SCH.FOO_BAR")).isEqualTo("SCH.FOO_BAR");
}
}
@@ -31,11 +31,13 @@ public class ServerConfigTest {
Properties props = new Properties();
props.setProperty("persistBatch", "INSERT");
props.setProperty("persistBatchOnCascade", "INSERT");
props.setProperty("dbuuid","binary");
serverConfig.loadFromProperties(props);
assertEquals(PersistBatch.INSERT, serverConfig.getPersistBatch());
assertEquals(PersistBatch.INSERT, serverConfig.getPersistBatchOnCascade());
assertEquals(ServerConfig.DbUuid.BINARY, serverConfig.getDbUuid());
serverConfig.setPersistBatch(PersistBatch.NONE);
serverConfig.setPersistBatchOnCascade(PersistBatch.NONE);
@@ -45,6 +47,7 @@ public class ServerConfigTest {
props1.setProperty("ebean.persistBatchOnCascade", "ALL");
serverConfig.loadFromProperties(props1);
serverConfig.loadTestProperties();
assertEquals(PersistBatch.ALL, serverConfig.getPersistBatch());
assertEquals(PersistBatch.ALL, serverConfig.getPersistBatchOnCascade());
@@ -35,7 +35,7 @@ public class ModelContainerApplyTest {
assertThat(foo.getComment()).isEqualTo("comment");
assertThat(foo.getTablespace()).isEqualTo("fooSpace");
assertThat(foo.getIndexTablespace()).isEqualTo("fooIndexSpace");
assertThat(foo.isWithHistory()).isEqualTo(true);
assertThat(foo.isWithHistory()).isEqualTo(false);
assertThat(foo.getColumns()).containsKeys("col1", "col3", "added_to_foo");
}
}
@@ -0,0 +1,32 @@
package com.avaje.ebeaninternal.server.lib;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import org.junit.Ignore;
import org.junit.Test;
public class ShutdownManagerTest extends BaseTestCase {
/**
* Run this test manually.
*/
@Ignore
@Test
public void test_deregisterShutdownHook() {
Ebean.getDefaultServer();
ShutdownManager.deregisterShutdownHook();
}
/**
* Run this test manually.
*/
@Ignore
@Test
public void test_noShutdownHook() {
System.setProperty("ebean.registerShutdownHook","false");
Ebean.getDefaultServer();
}
}
@@ -0,0 +1,35 @@
package com.avaje.ebeaninternal.server.type;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.config.dbplatform.PostgresPlatform;
import com.avaje.ebeaninternal.server.core.BootupClasses;
import org.junit.Test;
import static org.junit.Assert.*;
public class DefaultTypeManagerTest {
DefaultTypeManager typeManager;
public DefaultTypeManagerTest() {
ServerConfig serverConfig = new ServerConfig();
serverConfig.setDatabasePlatform(new PostgresPlatform());
BootupClasses bootupClasses = new BootupClasses();
typeManager = new DefaultTypeManager(serverConfig, bootupClasses);
}
@Test
public void isIntegerType() {
assertTrue(typeManager.isIntegerType("1"));
assertTrue(typeManager.isIntegerType("0"));
assertFalse(typeManager.isIntegerType("A"));
assertFalse(typeManager.isIntegerType("01"));
assertFalse(typeManager.isIntegerType(" 01"));
assertFalse(typeManager.isIntegerType(" 0"));
assertFalse(typeManager.isIntegerType(" 1"));
assertFalse(typeManager.isIntegerType(" A"));
}
}
@@ -1,22 +1,22 @@
package com.avaje.ebeaninternal.server.type;
import java.util.List;
import java.util.UUID;
import org.junit.Assert;
import org.junit.Test;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.SqlRow;
import com.avaje.tests.model.basic.UUOne;
import org.junit.Test;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class TestBinaryUUID extends BaseTestCase {
@Test
public void test() {
UUOne one0 = new UUOne();
one0.setName("first one");
@@ -27,26 +27,31 @@ public class TestBinaryUUID extends BaseTestCase {
Ebean.save(one0);
Ebean.save(one1);
UUOne fetch0 = Ebean.find(UUOne.class, one0.getId());
UUOne fetch1 = Ebean.find(UUOne.class, one1.getId());
Assert.assertEquals(one0.getId(), fetch0.getId());
Assert.assertEquals(one0.getName(), fetch0.getName());
Assert.assertEquals(one1.getId(), fetch1.getId());
Assert.assertEquals(one1.getName(), fetch1.getName());
assertEquals(one0.getId(), fetch0.getId());
assertEquals(one0.getName(), fetch0.getName());
assertEquals(one1.getId(), fetch1.getId());
assertEquals(one1.getName(), fetch1.getName());
String sql = "select id, name from uuone";
List<SqlRow> list = Ebean.createSqlQuery(sql).findList();
for (SqlRow sqlRow : list) {
Object sqlId = sqlRow.get("id");
Assert.assertNotNull(sqlId);
Assert.assertTrue(sqlId instanceof byte[]);
assertNotNull(sqlId);
UUID uuid = sqlRow.getUUID("id");
Assert.assertNotNull(uuid);
assertNotNull(uuid);
}
String asJson = Ebean.json().toJson(fetch0);
UUOne bean = Ebean.json().toBean(UUOne.class, asJson);
assertEquals(fetch0.getId(), bean.getId());
assertEquals(fetch0.getName(), bean.getName());
}
}
@@ -1,5 +1,6 @@
package com.avaje.tests.basic.type;
import com.avaje.tests.model.basic.EBasicEnumInt;
import org.junit.Assert;
import org.junit.Test;
@@ -11,6 +12,9 @@ import com.avaje.tests.model.basic.EBasic;
import com.avaje.tests.model.basic.EBasic.Status;
import com.avaje.tests.model.basic.EBasicEnumId;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class TestEnumValueAnnotation extends BaseTestCase {
@Test
@@ -28,7 +32,7 @@ public class TestEnumValueAnnotation extends BaseTestCase {
SqlRow sqlRow = q.findUnique();
String strStatus = sqlRow.getString("status");
Assert.assertEquals("N", strStatus);
assertEquals("N", strStatus);
EBasic b2 = new EBasic();
b2.setName("Apple");
@@ -40,9 +44,9 @@ public class TestEnumValueAnnotation extends BaseTestCase {
b3.setName("Orange");
Ebean.save(b3);
}
@Test
public void testAsId() {
EBasicEnumId b = new EBasicEnumId();
b.setName("Banana");
@@ -56,7 +60,7 @@ public class TestEnumValueAnnotation extends BaseTestCase {
SqlRow sqlRow = q.findUnique();
String strStatus = sqlRow.getString("status");
Assert.assertEquals("N", strStatus);
assertEquals("N", strStatus);
try {
b = Ebean.find(EBasicEnumId.class, b.getStatus());
@@ -64,7 +68,32 @@ public class TestEnumValueAnnotation extends BaseTestCase {
Assert.fail("The use of an enum as id should work : " + iae.getLocalizedMessage());
}
Assert.assertEquals(EBasicEnumId.Status.NEW, b.getStatus());
assertEquals(EBasicEnumId.Status.NEW, b.getStatus());
}
@Test
public void testDbEnumValueInt() {
EBasicEnumInt b = new EBasicEnumInt();
b.setName("Banana");
b.setStatus(EBasicEnumInt.Status.NEW);
Ebean.save(b);
SqlQuery q = Ebean.createSqlQuery("select * from e_basic_eni where id = :id");
q.setParameter("id", b.getId());
SqlRow sqlRow = q.findUnique();
Integer intStatus = sqlRow.getInteger("status");
assertEquals(Integer.valueOf(1), intStatus);
EBasicEnumInt b2 = Ebean.find(EBasicEnumInt.class)
.where().eq("id", b.getId())
.eq("status", EBasicEnumInt.Status.NEW)
.findUnique();
assertNotNull(b2);
}
}
@@ -1,16 +1,19 @@
package com.avaje.tests.cascade;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import org.junit.Test;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.bean.BeanCollection;
import com.avaje.tests.model.basic.TSDetail;
import com.avaje.tests.model.basic.TSMaster;
import org.avaje.ebeantest.LoggedSqlCollector;
import org.junit.Test;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class TestPrivateOwned extends BaseTestCase {
@@ -33,9 +36,9 @@ public class TestPrivateOwned extends BaseTestCase {
BeanCollection<?> bc = (BeanCollection<?>) details;
Set<?> modifyRemovals = bc.getModifyRemovals();
Assert.assertNotNull(modifyRemovals);
Assert.assertTrue(modifyRemovals.size() == 1);
Assert.assertTrue(modifyRemovals.contains(removedDetail));
assertNotNull(modifyRemovals);
assertTrue(modifyRemovals.size() == 1);
assertTrue(modifyRemovals.contains(removedDetail));
Ebean.save(master);
@@ -43,11 +46,42 @@ public class TestPrivateOwned extends BaseTestCase {
List<TSDetail> detailsReload = masterReload.getDetails();
// the removed bean has really been removed
Assert.assertTrue(detailsReload.size() == 1);
assertTrue(detailsReload.size() == 1);
TSMaster master3 = Ebean.find(TSMaster.class, m0.getId());
List<TSDetail> details3 = master3.getDetails();
details3.clear();
}
@Test
public void insertThenUpdate() {
TSMaster m0 = new TSMaster();
m0.setName("m2");
m0.addDetail(new TSDetail("m2 detail 1"));
m0.addDetail(new TSDetail("m2 detail 2"));
Ebean.save(m0);
assertThat(m0.getDetails()).hasSize(2);
LoggedSqlCollector.start();
m0.getDetails().remove(0);
Ebean.save(m0);
List<String> loggedSql = LoggedSqlCollector.stop();
assertThat(loggedSql).hasSize(1);
assertThat(loggedSql.get(0)).contains("delete from t_detail_with_other_namexxxyy where id=?");
TSMaster masterReload = Ebean.find(TSMaster.class, m0.getId());
assertThat(masterReload.getDetails()).hasSize(1);
LoggedSqlCollector.start();
Ebean.save(m0);
loggedSql = LoggedSqlCollector.stop();
assertThat(loggedSql).hasSize(0);
}
}
@@ -5,6 +5,8 @@ import com.avaje.ebean.Ebean;
import com.avaje.ebean.EbeanServer;
import com.avaje.ebean.Query;
import com.avaje.tests.model.basic.Contact;
import com.avaje.tests.model.basic.Customer;
import com.avaje.tests.model.basic.ResetBasicData;
import org.avaje.ebeantest.LoggedSqlCollector;
import org.junit.Test;
@@ -18,6 +20,10 @@ public class TestDeleteByQuery extends BaseTestCase {
public void test() {
EbeanServer server = Ebean.getDefaultServer();
if (server.getName().equals("mysql")) {
// MySql does not the sub query selecting from the delete table
return;
}
Query<Contact> query = server.find(Contact.class).where().eq("group.name", "NahYeahMaybe").query();
@@ -45,4 +51,29 @@ public class TestDeleteByQuery extends BaseTestCase {
assertThat(list).isEmpty();
}
@Test
public void testCommit() {
EbeanServer server = Ebean.getDefaultServer();
if (server.getName().equals("mysql")) {
// MySql does not the sub query selecting from the delete table
return;
}
ResetBasicData.reset();
List<Customer> all = Customer.find.all();
Contact contact = new Contact();
contact.setFirstName("DelByQueryFirstName");
contact.setLastName("deleteMe");
contact.setCustomer(all.get(0));
Ebean.save(contact);
Ebean.find(Contact.class).select("id").where().eq("firstName", "DelByQueryFirstName").delete();
Contact contactFind = Ebean.find(Contact.class, contact.getId());
assertThat(contactFind).isNull();
}
}
@@ -0,0 +1,91 @@
package com.avaje.tests.history;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.SqlQuery;
import com.avaje.ebean.SqlRow;
import com.avaje.ebean.Version;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
import com.avaje.tests.model.converstation.User;
import org.junit.Test;
import java.sql.Timestamp;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class TestHistoryInsert extends BaseTestCase {
@Test
public void test() {
SpiEbeanServer defaultServer = (SpiEbeanServer)Ebean.getDefaultServer();
if (!"h2".equals(defaultServer.getDatabasePlatform().getName())) {
// Oracle for example uses total recall so we can select the explicit
// history tables as we do in this test
return;
}
User user = new User();
user.setName("Jim");
user.setEmail("one@email.com");
Ebean.save(user);
Timestamp afterInsert = new Timestamp(System.currentTimeMillis());
List<SqlRow> history = fetchHistory(user);
assertThat(history).isEmpty();
List<Version<User>> versions = Ebean.find(User.class).setId(user.getId()).findVersions();
assertThat(versions).hasSize(1);
user.setName("Jim v2");
Ebean.save(user);
history = fetchHistory(user);
assertThat(history).hasSize(1);
assertThat(history.get(0).getString("name")).isEqualTo("Jim");
versions = Ebean.find(User.class).setId(user.getId()).findVersions();
assertThat(versions).hasSize(2);
assertThat(versions.get(0).getDiff()).containsKeys("name", "version", "whenModified");
user.setName("Jim v3");
user.setEmail("three@email.com");
Ebean.save(user);
history = fetchHistory(user);
assertThat(history).hasSize(2);
assertThat(history.get(1).getString("name")).isEqualTo("Jim v2");
assertThat(history.get(1).getString("email")).isEqualTo("one@email.com");
versions = Ebean.find(User.class).setId(user.getId()).findVersions();
assertThat(versions).hasSize(3);
assertThat(versions.get(0).getDiff()).containsKeys("name", "email", "version", "whenModified");
Ebean.delete(user);
User earlyVersion = Ebean.find(User.class).setId(user.getId()).asOf(afterInsert).findUnique();
assertThat(earlyVersion.getName()).isEqualTo("Jim");
assertThat(earlyVersion.getEmail()).isEqualTo("one@email.com");
history = fetchHistory(user);
assertThat(history).hasSize(3);
assertThat(history.get(2).getString("name")).isEqualTo("Jim v3");
assertThat(history.get(2).getString("email")).isEqualTo("three@email.com");
versions = Ebean.find(User.class).setId(user.getId()).findVersions();
assertThat(versions).hasSize(3);
}
/**
* Use SqlQuery to query the history table directly.
*/
private List<SqlRow> fetchHistory(User user) {
SqlQuery sqlQuery = Ebean.createSqlQuery("select * from c_user_history where id = :id order by sys_period_start");
sqlQuery.setParameter("id", user.getId());
return sqlQuery.findList();
}
}
@@ -1,10 +1,5 @@
package com.avaje.tests.inheritance;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.bean.BeanCollection.ModifyListenMode;
@@ -13,6 +8,11 @@ import com.avaje.tests.model.basic.Animal;
import com.avaje.tests.model.basic.AnimalShelter;
import com.avaje.tests.model.basic.Cat;
import com.avaje.tests.model.basic.Dog;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertNotNull;
public class TestInheritanceOnMany extends BaseTestCase {
@@ -40,8 +40,9 @@ public class TestInheritanceOnMany extends BaseTestCase {
BeanList<?> beanList = (BeanList<?>)animals;
ModifyListenMode modifyListenMode = beanList.getModifyListenMode();
Assert.assertNotNull(modifyListenMode);
assertNotNull(modifyListenMode);
assertNotNull(Ebean.find(Animal.class).findList());
}
}
@@ -1,11 +1,15 @@
package com.avaje.tests.iud;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.tests.model.info.InfoCompany;
import com.avaje.tests.model.info.InfoCustomer;
import org.junit.Assert;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
public class TestInfoOneToOne extends BaseTestCase {
@Test
@@ -20,13 +24,17 @@ public class TestInfoOneToOne extends BaseTestCase {
customer.save();
// assert JsonIgnore working as expected
String asJson = Ebean.json().toJson(company);
assertThat(asJson).doesNotContain("contacts");
// assert both are inserted
Assert.assertNotNull(customer.getId());
Assert.assertNotNull(company.getId());
assertNotNull(customer.getId());
assertNotNull(company.getId());
// both can be fetched
Assert.assertNotNull(InfoCustomer.find.byId(customer.getId()));
Assert.assertNotNull(InfoCompany.find.byId(company.getId()));
assertNotNull(InfoCustomer.find.byId(customer.getId()));
assertNotNull(InfoCompany.find.byId(company.getId()));
// just update the customer
@@ -47,8 +55,8 @@ public class TestInfoOneToOne extends BaseTestCase {
// delete both customer and company
fetchedCustomer.delete();
Assert.assertNull(InfoCustomer.find.byId(customer.getId()));
Assert.assertNull(InfoCompany.find.byId(company.getId()));
assertNull(InfoCustomer.find.byId(customer.getId()));
assertNull(InfoCompany.find.byId(company.getId()));
}
@@ -0,0 +1,52 @@
package com.avaje.tests.m2m;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.SqlQuery;
import com.avaje.ebean.SqlRow;
import com.avaje.tests.model.basic.MnocRole;
import com.avaje.tests.model.basic.MnocUser;
import com.avaje.tests.model.m2m.MnyA;
import com.avaje.tests.model.m2m.MnyB;
import com.avaje.tests.model.m2m.MnyC;
import org.junit.Assert;
import org.junit.Test;
import javax.persistence.PersistenceException;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class TestM2MDeleteCascadeFromParent extends BaseTestCase {
@Test
public void test() {
MnyC c0 = new MnyC();
c0.setName("c0");
c0.save();
MnyA a0 = new MnyA();
a0.setName("a0");
a0.save();
MnyB b0 = new MnyB();
b0.setName("b0");
b0.setA(a0);
b0.save();
c0.getBs().add(b0);
Ebean.saveAssociation(c0, "bs");
SqlQuery sqlQuery = Ebean.createSqlQuery("select count(*) as count from mny_b_mny_c");
SqlRow unique = sqlQuery.findUnique();
assertEquals(Long.valueOf(1), unique.getLong("count"));
//Ebean.deleteManyToManyAssociations(b0, "cs");
a0.delete();
}
}
@@ -0,0 +1,31 @@
package com.avaje.tests.m2m;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.tests.model.m2m.MailBox;
import com.avaje.tests.model.m2m.MailUser;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class TestM2MMultipleLists extends BaseTestCase {
@Test
public void test() {
MailUser u = new MailUser();
u.setName("mailUser1");
MailBox m1 = new MailBox();
m1.setName("mailBox1");
u.getInbox().add(m1);
Ebean.save(u);
u = Ebean.find(MailUser.class, u.getId());
assertEquals(1, u.getInbox().size());
assertEquals(0, u.getOutbox().size());
}
}
@@ -0,0 +1,29 @@
package com.avaje.tests.m2m;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.tests.model.m2m.MnyTopic;
import org.junit.Test;
/**
* Added to test DDL generation for ManyToMany related back to itself.
*/
public class TestM2MSelfRelationship extends BaseTestCase {
@Test
public void test() {
Ebean.getDefaultServer();
// Create 2 roles r0 and r1
MnyTopic r0 = new MnyTopic("r0");
MnyTopic r1 = new MnyTopic("r1");
// Save r1 and r2
Ebean.save(r0);
Ebean.save(r1);
r0.getSubTopics().add(r1);
Ebean.save(r0);
}
}
@@ -68,4 +68,28 @@ public class TestM2mDeleteObject extends BaseTestCase {
Assert.assertEquals(2, permissionList2.size());
}
@Test
public void test_deleteCascading() {
Permission p1 = new Permission();
Permission p2 = new Permission();
p1.setName("p1");
p2.setName("p2");
Ebean.save(p1);
Ebean.save(p2);
Role role1 = new Role();
role1.setName("role");
Set<Permission> permissions = new HashSet<Permission>();
permissions.add(p2);
role1.setPermissions(permissions);
Ebean.save(role1);
// only deletes as not associated
Ebean.delete(p1);
// delete cascades
Ebean.delete(role1);
}
}
@@ -1,5 +1,6 @@
package com.avaje.tests.model.basic;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.Entity;
import javax.persistence.Id;
@@ -21,6 +22,9 @@ public abstract class Animal {
@Version
Long version;
@Column(name="species", insertable = false, updatable = false, nullable = false)
String species;
@ManyToOne
AnimalShelter shelter;
@@ -40,6 +44,14 @@ public abstract class Animal {
this.version = version;
}
public String getSpecies() {
return species;
}
public void setSpecies(String species) {
this.species = species;
}
public AnimalShelter getShelter() {
return shelter;
}
@@ -2,7 +2,7 @@ package com.avaje.tests.model.basic;
import com.avaje.ebean.annotation.ChangeLog;
import com.avaje.ebean.annotation.ChangeLogInsertMode;
import com.avaje.ebean.annotation.EnumValue;
import com.avaje.ebean.annotation.DbEnumValue;
import com.avaje.ebean.annotation.JsonIgnore;
import com.avaje.ebean.annotation.Where;
import com.avaje.tests.model.basic.finder.CustomerFinder;
@@ -36,14 +36,19 @@ public class Customer extends BasicDomain {
* EnumValue is an Ebean specific mapping for enums.
*/
public enum Status {
@EnumValue("N")
NEW,
NEW("N"),
ACTIVE("A"),
INACTIVE("I");
@EnumValue("A")
ACTIVE,
String dbValue;
Status(String dbValue) {
this.dbValue = dbValue;
}
@EnumValue("I")
INACTIVE
@DbEnumValue
public String getValue() {
return dbValue;
}
}
@Transient
@@ -0,0 +1,81 @@
package com.avaje.tests.model.basic;
import com.avaje.ebean.annotation.DbEnumType;
import com.avaje.ebean.annotation.DbEnumValue;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.sql.Timestamp;
@Entity
@Table(name = "e_basic_eni")
public class EBasicEnumInt {
public enum Status {
NEW("1"),
ACTIVE("2"),
INACTIVE("3");
String value;
Status(String value) {
this.value = value;
}
@DbEnumValue(storage = DbEnumType.INTEGER)
public String getValue() {
return value;
}
}
@Id
Integer id;
Status status;
String name;
String description;
Timestamp someDate;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Timestamp getSomeDate() {
return someDate;
}
public void setSomeDate(Timestamp someDate) {
this.someDate = someDate;
}
}
@@ -1,14 +1,14 @@
package com.avaje.tests.model.basic;
import java.sql.Date;
import java.util.ArrayList;
import java.util.List;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.EbeanServer;
import com.avaje.ebean.TxRunnable;
import com.avaje.tests.model.basic.Order.Status;
import java.sql.Date;
import java.util.ArrayList;
import java.util.List;
public class ResetBasicData {
private static boolean runOnce;
@@ -135,6 +135,7 @@ public class ResetBasicData {
createOrder2(cust2);
createOrder3(cust1);
createOrder4(cust1);
createOrder5(cust2);
}
});
}
@@ -313,4 +314,13 @@ public class ResetBasicData {
Ebean.save(order);
}
private void createOrder5(Customer customer) {
Order order = new Order();
order.setCustomer(customer);
order.addShipment(new OrderShipment());
Ebean.save(order);
}
}

Some files were not shown because too many files have changed in this diff Show More