list = findSingleAttributeList();
return !list.isEmpty() ? list.get(0) : null;
diff --git a/ebean-api/src/main/java/io/ebean/Transaction.java b/ebean-api/src/main/java/io/ebean/Transaction.java
index 065396a925..21ce076a8f 100644
--- a/ebean-api/src/main/java/io/ebean/Transaction.java
+++ b/ebean-api/src/main/java/io/ebean/Transaction.java
@@ -260,6 +260,12 @@ static Transaction current() {
*/
void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties);
+ /**
+ * If set to false (default is true) generated propertes are only set, if it is the version property or have a null value.
+ * This may be useful in backup & restore scenarios, if you want set WhenCreated/WhenModified.
+ */
+ void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties);
+
/**
* Set if the L2 cache should be skipped for "find by id" and "find by natural key" queries.
*
diff --git a/ebean-api/src/main/java/io/ebean/annotation/ext/IntersectionFactory.java b/ebean-api/src/main/java/io/ebean/annotation/ext/IntersectionFactory.java
new file mode 100644
index 0000000000..5749549fb9
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/annotation/ext/IntersectionFactory.java
@@ -0,0 +1,29 @@
+package io.ebean.annotation.ext;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Annotation to define a factory for an intersection model. This class MUST have a constructor or factory method with two parameters that accepts parent and property type.
+ * @author Roland Praml, FOCONIS AG
+ */
+@Documented
+@Target({ FIELD, TYPE })
+@Retention(RUNTIME)
+public @interface IntersectionFactory {
+
+ /**
+ * The intersection model class.
+ */
+ Class value();
+
+ /**
+ * An optional factory method.
+ */
+ String factoryMethod() default "";
+}
diff --git a/ebean-api/src/main/java/io/ebean/annotation/ext/package.html b/ebean-api/src/main/java/io/ebean/annotation/ext/package.html
new file mode 100644
index 0000000000..9505c002ca
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/annotation/ext/package.html
@@ -0,0 +1,8 @@
+
+
+ Ebean Annotations
+
+
+This classes will be moved later to the ebean-annotation module.
+
+
diff --git a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java
index 1161476ddd..dcb002266f 100644
--- a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java
+++ b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java
@@ -162,7 +162,17 @@ enum ModifyListenMode {
*
* For maps this returns the entrySet as we need the keys of the map.
*/
- Collection> actualEntries();
+ Collection> actualEntries(boolean load);
+
+ default Collection> actualEntries() {
+ return actualEntries(false);
+ }
+ /**
+ * Returns entries, that were lazily added at the end of the list. Might be null.
+ */
+ default Collection getLazyAddedEntries(boolean reset) {
+ return null;
+ }
/**
* return true if there are real rows held. Return false is this is using
@@ -240,4 +250,9 @@ enum ModifyListenMode {
* Return a shallow copy of this collection that is modifiable.
*/
BeanCollection shallowCopy();
+
+ /**
+ * Clears the underlying collection.
+ */
+ void clear();
}
diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBean.java b/ebean-api/src/main/java/io/ebean/bean/EntityBean.java
index 53bfc9ba49..1a70a68571 100644
--- a/ebean-api/src/main/java/io/ebean/bean/EntityBean.java
+++ b/ebean-api/src/main/java/io/ebean/bean/EntityBean.java
@@ -41,6 +41,13 @@ default Object _ebean_newInstanceReadOnly() {
throw new NotEnhancedException();
}
+ /**
+ * Creates a new instance and uses the provided intercept. (For EntityExtension)
+ */
+ default Object _ebean_newExtendedInstance(int offset, EntityBean base) {
+ throw new NotEnhancedException();
+ }
+
/**
* Generated method that sets the loaded state on all the embedded beans on
* this entity bean by using EntityBeanIntercept.setEmbeddedLoaded(Object o);
@@ -120,4 +127,19 @@ default Object _ebean_getFieldIntercept(int fieldIndex) {
default void toString(ToStringBuilder builder) {
throw new NotEnhancedException();
}
+
+ /**
+ * Returns the ExtensionAccessors, this is always NONE
for non extendable beans.
+ */
+ default ExtensionAccessors _ebean_getExtensionAccessors() {
+ return ExtensionAccessors.NONE;
+ }
+
+ /**
+ * Returns the extension bean for an accessor. This will throw NotEnhancedException for non extendable beans.
+ * (It is not intended to call this method here)
+ */
+ default EntityBean _ebean_getExtension(ExtensionAccessor accessor) {
+ throw new NotEnhancedException(); // not an extendableBean
+ }
}
diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java
index 1127c02b25..120068ad34 100644
--- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java
+++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java
@@ -552,4 +552,24 @@ public interface EntityBeanIntercept extends Serializable {
* Update the 'next' mutable info returning the content that was obtained via dirty detection.
*/
String mutableNext(int propertyIndex);
+
+ /**
+ * Returns the value of the property. Can also return virtual properties.
+ */
+ Object value(int propertyIndex);
+
+ /**
+ * Returns the value of the property with intercept access. Can also return virtual properties.
+ */
+ Object valueIntercept(int propertyIndex);
+
+ /**
+ * Writes the value to the property. Can also write virtual properties.
+ */
+ void setValue(int propertyIndex, Object value);
+
+ /**
+ * Writes the value to the property with intercept access. Can also write virtual properties.
+ */
+ void setValueIntercept(int propertyIndex, Object value);
}
diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java
new file mode 100644
index 0000000000..9efdd8ee07
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java
@@ -0,0 +1,562 @@
+package io.ebean.bean;
+
+import io.ebean.ValuePair;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Intercept for classes annotated with @EntityExtension. The intercept will delegate all calls to the base intercept of the
+ * ExtendableBean and adds given offset to all field operations.
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public class EntityExtensionIntercept implements EntityBeanIntercept {
+ private final EntityBeanIntercept base;
+ private final int offset;
+
+ public EntityExtensionIntercept(Object ownerBean, int offset, EntityBean base) {
+ this.base = base._ebean_getIntercept();
+ this.offset = offset;
+ }
+
+ @Override
+ public EntityBean owner() {
+ return base.owner();
+ }
+
+ @Override
+ public PersistenceContext persistenceContext() {
+ return base.persistenceContext();
+ }
+
+ @Override
+ public void setPersistenceContext(PersistenceContext persistenceContext) {
+ base.setPersistenceContext(persistenceContext);
+ }
+
+ @Override
+ public void setNodeUsageCollector(NodeUsageCollector usageCollector) {
+ base.setNodeUsageCollector(usageCollector);
+ }
+
+ @Override
+ public Object ownerId() {
+ return base.ownerId();
+ }
+
+ @Override
+ public void setOwnerId(Object ownerId) {
+ base.setOwnerId(ownerId);
+ }
+
+ @Override
+ public Object embeddedOwner() {
+ return base.embeddedOwner();
+ }
+
+ @Override
+ public int embeddedOwnerIndex() {
+ return base.embeddedOwnerIndex();
+ }
+
+ @Override
+ public void clearGetterCallback() {
+ base.clearGetterCallback();
+ }
+
+ @Override
+ public void registerGetterCallback(PreGetterCallback getterCallback) {
+ base.registerGetterCallback(getterCallback);
+ }
+
+ @Override
+ public void setEmbeddedOwner(EntityBean parentBean, int embeddedOwnerIndex) {
+ base.setEmbeddedOwner(parentBean, embeddedOwnerIndex);
+ }
+
+ @Override
+ public void setBeanLoader(BeanLoader beanLoader, PersistenceContext ctx) {
+ base.setBeanLoader(beanLoader, ctx);
+ }
+
+ @Override
+ public void setBeanLoader(BeanLoader beanLoader) {
+ base.setBeanLoader(beanLoader);
+ }
+
+ @Override
+ public boolean isFullyLoadedBean() {
+ return base.isFullyLoadedBean();
+ }
+
+ @Override
+ public void setFullyLoadedBean(boolean fullyLoadedBean) {
+ base.setFullyLoadedBean(fullyLoadedBean);
+ }
+
+ @Override
+ public boolean isPartial() {
+ return base.isPartial();
+ }
+
+ @Override
+ public boolean isDirty() {
+ return base.isDirty();
+ }
+
+ @Override
+ public void setEmbeddedDirty(int embeddedProperty) {
+ base.setEmbeddedDirty(embeddedProperty + offset);
+ }
+
+ @Override
+ public void setDirty(boolean dirty) {
+ base.setDirty(dirty);
+ }
+
+ @Override
+ public boolean isNew() {
+ return base.isNew();
+ }
+
+ @Override
+ public boolean isNewOrDirty() {
+ return base.isNewOrDirty();
+ }
+
+ @Override
+ public boolean hasIdOnly(int idIndex) {
+ return base.hasIdOnly(idIndex + offset);
+ }
+
+ @Override
+ public boolean isReference() {
+ return base.isReference();
+ }
+
+ @Override
+ public void setReference(int idPos) {
+ base.setReference(idPos + offset);
+ }
+
+ @Override
+ public void setLoadedFromCache(boolean loadedFromCache) {
+ base.setLoadedFromCache(loadedFromCache);
+ }
+
+ @Override
+ public boolean isLoadedFromCache() {
+ return base.isLoadedFromCache();
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return base.isReadOnly();
+ }
+
+ @Override
+ public void setReadOnly(boolean readOnly) {
+ base.setReadOnly(readOnly);
+ }
+
+ @Override
+ public void setForceUpdate(boolean forceUpdate) {
+ base.setForceUpdate(forceUpdate);
+ }
+
+ @Override
+ public boolean isUpdate() {
+ return base.isUpdate();
+ }
+
+ @Override
+ public boolean isLoaded() {
+ return base.isLoaded();
+ }
+
+ @Override
+ public void setNew() {
+ base.setNew();
+ }
+
+ @Override
+ public void setLoaded() {
+ base.setLoaded();
+ }
+
+ @Override
+ public void setLoadedLazy() {
+ base.setLoadedLazy();
+ }
+
+ @Override
+ public void setLazyLoadFailure(Object ownerId) {
+ base.setLazyLoadFailure(ownerId);
+ }
+
+ @Override
+ public boolean isLazyLoadFailure() {
+ return base.isLazyLoadFailure();
+ }
+
+ @Override
+ public boolean isDisableLazyLoad() {
+ return base.isDisableLazyLoad();
+ }
+
+ @Override
+ public void setDisableLazyLoad(boolean disableLazyLoad) {
+ base.setDisableLazyLoad(disableLazyLoad);
+ }
+
+ @Override
+ public void setEmbeddedLoaded(Object embeddedBean) {
+ base.setEmbeddedLoaded(embeddedBean);
+ }
+
+ @Override
+ public boolean isEmbeddedNewOrDirty(Object embeddedBean) {
+ return base.isEmbeddedNewOrDirty(embeddedBean);
+ }
+
+ @Override
+ public Object origValue(int propertyIndex) {
+ return base.origValue(propertyIndex + offset);
+ }
+
+ @Override
+ public int findProperty(String propertyName) {
+ return base.findProperty(propertyName);
+ }
+
+ @Override
+ public String property(int propertyIndex) {
+ return base.property(propertyIndex + offset);
+ }
+
+ @Override
+ public int propertyLength() {
+ return base.propertyLength();
+ }
+
+ @Override
+ public void setPropertyLoaded(String propertyName, boolean loaded) {
+ base.setPropertyLoaded(propertyName, loaded);
+ }
+
+ @Override
+ public void setPropertyUnloaded(int propertyIndex) {
+ base.setPropertyUnloaded(propertyIndex + offset);
+ }
+
+ @Override
+ public void setLoadedProperty(int propertyIndex) {
+ base.setLoadedProperty(propertyIndex + offset);
+ }
+
+ @Override
+ public void setLoadedPropertyAll() {
+ base.setLoadedPropertyAll();
+ }
+
+ @Override
+ public boolean isLoadedProperty(int propertyIndex) {
+ return base.isLoadedProperty(propertyIndex + offset);
+ }
+
+ @Override
+ public boolean isChangedProperty(int propertyIndex) {
+ return base.isChangedProperty(propertyIndex + offset);
+ }
+
+ @Override
+ public boolean isDirtyProperty(int propertyIndex) {
+ return base.isDirtyProperty(propertyIndex + offset);
+ }
+
+ @Override
+ public void markPropertyAsChanged(int propertyIndex) {
+ base.markPropertyAsChanged(propertyIndex + offset);
+ }
+
+ @Override
+ public void setChangedProperty(int propertyIndex) {
+ base.setChangedProperty(propertyIndex + offset);
+ }
+
+ @Override
+ public void setChangeLoaded(int propertyIndex) {
+ base.setChangeLoaded(propertyIndex + offset);
+ }
+
+ @Override
+ public void setEmbeddedPropertyDirty(int propertyIndex) {
+ base.setEmbeddedPropertyDirty(propertyIndex + offset);
+ }
+
+ @Override
+ public void setOriginalValue(int propertyIndex, Object value) {
+ base.setOriginalValue(propertyIndex + offset, value);
+ }
+
+ @Override
+ public void setOriginalValueForce(int propertyIndex, Object value) {
+ base.setOriginalValueForce(propertyIndex + offset, value);
+ }
+
+ @Override
+ public void setNewBeanForUpdate() {
+ base.setNewBeanForUpdate();
+ }
+
+ @Override
+ public Set loadedPropertyNames() {
+ return base.loadedPropertyNames();
+ }
+
+ @Override
+ public boolean[] dirtyProperties() {
+ return base.dirtyProperties();
+ }
+
+ @Override
+ public Set dirtyPropertyNames() {
+ return base.dirtyPropertyNames();
+ }
+
+ @Override
+ public void addDirtyPropertyNames(Set props, String prefix) {
+ base.addDirtyPropertyNames(props, prefix);
+ }
+
+ @Override
+ public boolean hasDirtyProperty(Set propertyNames) {
+ return base.hasDirtyProperty(propertyNames);
+ }
+
+ @Override
+ public Map dirtyValues() {
+ return base.dirtyValues();
+ }
+
+ @Override
+ public void addDirtyPropertyValues(Map dirtyValues, String prefix) {
+ base.addDirtyPropertyValues(dirtyValues, prefix);
+ }
+
+ @Override
+ public void addDirtyPropertyValues(BeanDiffVisitor visitor) {
+ base.addDirtyPropertyValues(visitor);
+ }
+
+ @Override
+ public StringBuilder dirtyPropertyKey() {
+ return base.dirtyPropertyKey();
+ }
+
+ @Override
+ public void addDirtyPropertyKey(StringBuilder sb) {
+ base.addDirtyPropertyKey(sb);
+ }
+
+ @Override
+ public StringBuilder loadedPropertyKey() {
+ return base.loadedPropertyKey();
+ }
+
+ @Override
+ public boolean[] loaded() {
+ return base.loaded();
+ }
+
+ @Override
+ public int lazyLoadPropertyIndex() {
+ return base.lazyLoadPropertyIndex() - offset;
+ }
+
+ @Override
+ public String lazyLoadProperty() {
+ return base.lazyLoadProperty();
+ }
+
+ @Override
+ public void loadBean(int loadProperty) {
+ base.loadBean(loadProperty);
+ }
+
+ @Override
+ public void loadBeanInternal(int loadProperty, BeanLoader loader) {
+ base.loadBeanInternal(loadProperty + offset, loader);
+ }
+
+ @Override
+ public void initialisedMany(int propertyIndex) {
+ base.initialisedMany(propertyIndex + offset);
+ }
+
+ @Override
+ public void preGetterCallback(int propertyIndex) {
+ base.preGetterCallback(propertyIndex + offset);
+ }
+
+ @Override
+ public void preGetId() {
+ base.preGetId();
+ }
+
+ @Override
+ public void preGetter(int propertyIndex) {
+ base.preGetter(propertyIndex + offset);
+ }
+
+ @Override
+ public void preSetterMany(boolean interceptField, int propertyIndex, Object oldValue, Object newValue) {
+ base.preSetterMany(interceptField, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void setChangedPropertyValue(int propertyIndex, boolean setDirtyState, Object origValue) {
+ base.setChangedPropertyValue(propertyIndex + offset, setDirtyState, origValue);
+ }
+
+ @Override
+ public void setDirtyStatus() {
+ base.setDirtyStatus();
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, Object oldValue, Object newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, boolean oldValue, boolean newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, int oldValue, int newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, long oldValue, long newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, double oldValue, double newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, float oldValue, float newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, short oldValue, short newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, char oldValue, char newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, byte oldValue, byte newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, char[] oldValue, char[] newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void preSetter(boolean intercept, int propertyIndex, byte[] oldValue, byte[] newValue) {
+ base.preSetter(intercept, propertyIndex + offset, oldValue, newValue);
+ }
+
+ @Override
+ public void setOldValue(int propertyIndex, Object oldValue) {
+ base.setOldValue(propertyIndex + offset, oldValue);
+ }
+
+ @Override
+ public int sortOrder() {
+ return base.sortOrder();
+ }
+
+ @Override
+ public void setSortOrder(int sortOrder) {
+ base.setSortOrder(sortOrder);
+ }
+
+ @Override
+ public void setDeletedFromCollection(boolean deletedFromCollection) {
+ base.setDeletedFromCollection(deletedFromCollection);
+ }
+
+ @Override
+ public boolean isOrphanDelete() {
+ return base.isOrphanDelete();
+ }
+
+ @Override
+ public void setLoadError(int propertyIndex, Exception t) {
+ base.setLoadError(propertyIndex + offset, t);
+ }
+
+ @Override
+ public Map loadErrors() {
+ return base.loadErrors();
+ }
+
+ @Override
+ public boolean isChangedProp(int propertyIndex) {
+ return base.isChangedProp(propertyIndex + offset);
+ }
+
+ @Override
+ public MutableValueInfo mutableInfo(int propertyIndex) {
+ return base.mutableInfo(propertyIndex + offset);
+ }
+
+ @Override
+ public void mutableInfo(int propertyIndex, MutableValueInfo info) {
+ base.mutableInfo(propertyIndex + offset, info);
+ }
+
+ @Override
+ public void mutableNext(int propertyIndex, MutableValueNext next) {
+ base.mutableNext(propertyIndex + offset, next);
+ }
+
+ @Override
+ public String mutableNext(int propertyIndex) {
+ return base.mutableNext(propertyIndex + offset);
+ }
+
+ @Override
+ public Object value(int propertyIndex) {
+ return base.value(propertyIndex + offset);
+ }
+
+ @Override
+ public Object valueIntercept(int propertyIndex) {
+ return base.valueIntercept(propertyIndex + offset);
+ }
+
+ @Override
+ public void setValue(int propertyIndex, Object value) {
+ base.setValue(propertyIndex + offset, value);
+ }
+
+ @Override
+ public void setValueIntercept(int propertyIndex, Object value) {
+ base.setValueIntercept(propertyIndex + offset, value);
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java
new file mode 100644
index 0000000000..cfea5db354
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java
@@ -0,0 +1,34 @@
+package io.ebean.bean;
+
+import io.ebean.bean.extend.ExtendableBean;
+
+/**
+ * Provides access to the EntityExtensions. Each ExtendableBean may have multiple Extension-Accessors stored in the static
+ * {@link ExtensionAccessors} per class level.
+ *
+ * This interface is internally used by the enhancer.
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public interface ExtensionAccessor {
+
+ /*
+ * Returns the extension for a given bean.
+ */
+ EntityBean getExtension(ExtendableBean bean);
+
+ /**
+ * Returns the index of this extension.
+ */
+ int getIndex();
+
+ /**
+ * Return the type of this extension.
+ */
+ Class> getType();
+
+ /**
+ * Returns the additional properties of this extension.
+ */
+ String[] getProperties();
+}
diff --git a/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java
new file mode 100644
index 0000000000..2a693bc97e
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java
@@ -0,0 +1,265 @@
+package io.ebean.bean;
+
+import io.ebean.bean.extend.EntityExtension;
+import io.ebean.bean.extend.ExtendableBean;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Each ExtendableBean has one static member defined as
+ *
+ * private static ExtensionAccessors _ebean_extension_accessors =
+ * new ExtensionAccessors(thisClass._ebeanProps, superClass._ebean_extension_accessors | null)
+ *
+ * The ExtensionAccessors class is used to compute the additional space, that has to be reserved
+ * in the descriptor and the virtual properties, that will be added to the bean descriptor.
+ * The following class structure:
+ *
+ * @Entity
+ * class Base extends ExtendableBean {
+ * String prop0;
+ * String prop1;
+ * String prop2;
+ * }
+ * @EntityExtends(Base.class)
+ * class Ext1 {
+ * String prop3;
+ * String prop4;
+ * }
+ * @EntityExtends(Base.class)
+ * class Ext2 {
+ * String prop5;
+ * String prop6;
+ * }
+ *
+ * will create an EntityBeanIntercept for "Base" holding up to 7 fields. Writing to fields 0..2 with ebi.setValue will modify
+ * the fields in Base, the r/w accesses to fields 3..4 are routed to Ext1 and 5..6 to Ext2.
+ *
+ * Note about offset and index:
+ *
+ *
+ * When you have subclasses (class Child extends Base
) the extensions have all the same index in the parent and in
+ * the subclass, but may have different offsets, as the Child-class will provide additional fields.
+ *
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public class ExtensionAccessors implements Iterable {
+
+ /**
+ * Default extension info for beans, that have no extension.
+ */
+ public static ExtensionAccessors NONE = new ExtensionAccessors();
+
+ /**
+ * The start offset specifies the offset where the first extension will start
+ */
+ private final int startOffset;
+
+ /**
+ * The entries.
+ */
+ private List accessors = new ArrayList<>();
+
+ /**
+ * If we inherit from a class that has extensions, we have to inherit also all extensions from here
+ */
+ private final ExtensionAccessors parent;
+
+ /**
+ * The total property length of all extensions. This will be initialized once and cannot be changed any more
+ */
+ private volatile int propertyLength = -1;
+
+ /**
+ * The offsets where the extensions will start for effective binary search.
+ */
+ private int[] offsets;
+
+ /**
+ * Lock for synchronizing the initialization.
+ */
+ private static final Lock lock = new ReentrantLock();
+
+ /**
+ * Constructor for ExtensionInfo.NONE
.
+ */
+ private ExtensionAccessors() {
+ this.startOffset = Integer.MAX_VALUE;
+ this.propertyLength = 0;
+ this.parent = null;
+ }
+
+ /**
+ * Called from enhancer. Each entity has a static field initialized with
+ * _ebean_extensions = new ExtensonInfo(thisClass._ebeanProps, superClass._ebean_extensions | null)
+ */
+ public ExtensionAccessors(String[] props, ExtensionAccessors parent) {
+ this.startOffset = props.length;
+ this.parent = parent;
+ }
+
+ /**
+ * Called from enhancer. Each class annotated with {@link EntityExtension} will show up here.
+ *
+ * @param prototype instance of the class that is annotated with {@link EntityExtension}
+ */
+ public ExtensionAccessor add(EntityBean prototype) {
+ if (propertyLength != -1) {
+ throw new UnsupportedOperationException("The extension is already in use and cannot be extended anymore");
+ }
+ Entry entry = new Entry(prototype);
+ lock.lock();
+ try {
+ accessors.add(entry);
+ } finally {
+ lock.unlock();
+ }
+ return entry;
+ }
+
+ /**
+ * returns how many extensions are registered.
+ */
+ public int size() {
+ init();
+ return accessors.size();
+ }
+
+ /**
+ * Returns the additional properties, that have been added by extensions.
+ */
+ public int getPropertyLength() {
+ init();
+ return propertyLength;
+ }
+
+ /**
+ * Copies parent extensions and initializes the offsets. This will be done once only.
+ */
+ private void init() {
+ if (propertyLength != -1) {
+ return;
+ }
+ lock.lock();
+ try {
+ if (propertyLength != -1) {
+ return;
+ }
+ if (parent != null) {
+ parent.init();
+ if (accessors.isEmpty()) {
+ accessors = parent.accessors;
+ } else {
+ accessors.addAll(0, parent.accessors);
+ }
+ }
+ int length = 0;
+ offsets = new int[accessors.size()];
+ for (int i = 0; i < accessors.size(); i++) {
+ Entry entry = (Entry) accessors.get(i);
+ entry.index = i;
+ offsets[i] = startOffset + length;
+ length += entry.getProperties().length;
+ }
+ propertyLength = length;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Returns the offset of this extension accessor.
+ * Note: The offset may vary on subclasses
+ */
+ int getOffset(ExtensionAccessor accessor) {
+ return offsets[accessor.getIndex()];
+ }
+
+ /**
+ * Finds the accessor for a given property. If the propertyIndex is lower than startOffset, no accessor will be returned,
+ * as this means that we try to access a property in the base-entity.
+ */
+ ExtensionAccessor findAccessor(int propertyIndex) {
+ init();
+ if (propertyIndex < startOffset) {
+ return null;
+ }
+ int pos = Arrays.binarySearch(offsets, propertyIndex);
+ if (pos == -1) {
+ return null;
+ }
+ if (pos < 0) {
+ pos = -2 - pos;
+ }
+ return accessors.get(pos);
+ }
+
+ @Override
+ public Iterator iterator() {
+ init();
+ return accessors.iterator();
+ }
+
+ /**
+ * Invoked by enhancer.
+ */
+ public EntityBean createInstance(ExtensionAccessor accessor, EntityBean base) {
+ int offset = getOffset(accessor);
+ return ((Entry) accessor).createInstance(offset, base);
+ }
+
+ static class Entry implements ExtensionAccessor {
+ private int index;
+ private final EntityBean prototype;
+
+ private Entry(EntityBean prototype) {
+ this.prototype = prototype;
+ }
+
+ @Override
+ public String[] getProperties() {
+ return prototype._ebean_getPropertyNames();
+ }
+
+ @Override
+ public int getIndex() {
+ return index;
+ }
+
+ @Override
+ public Class> getType() {
+ return prototype.getClass();
+ }
+
+ EntityBean createInstance(int offset, EntityBean base) {
+ return (EntityBean) prototype._ebean_newExtendedInstance(offset, base);
+ }
+
+ @Override
+ public EntityBean getExtension(ExtendableBean bean) {
+ EntityBean eb = (EntityBean) bean;
+ return eb._ebean_getExtension(Entry.this);
+ }
+ }
+
+ /**
+ * Reads the extension accessors for a given class. If the provided type is not an ExtenadableBean, the
+ * ExtensionAccessors.NONE
is returned.
+ */
+ public static ExtensionAccessors read(Class> type) {
+ if (ExtendableBean.class.isAssignableFrom(type)) {
+ try {
+ return (ExtensionAccessors) type.getField("_ebean_extension_accessors").get(null);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException("Could not read extension info from " + type, e);
+ }
+ }
+ return ExtensionAccessors.NONE;
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java b/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java
new file mode 100644
index 0000000000..a38655100b
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java
@@ -0,0 +1,116 @@
+package io.ebean.bean;
+
+/**
+ * Base class for InterceptReadOnly / InterceptReadWrite. This class should contain only the essential member variables to keep
+ * the memory footprint low.
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public abstract class InterceptBase implements EntityBeanIntercept {
+
+ /**
+ * The actual entity bean that 'owns' this intercept.
+ */
+ protected final EntityBean owner;
+
+ protected InterceptBase(EntityBean owner) {
+ this.owner = owner;
+ }
+
+ protected ExtensionAccessor findAccessor(int index) {
+ return owner._ebean_getExtensionAccessors().findAccessor(index);
+ }
+
+ private int getOffset(ExtensionAccessor accessor) {
+ return owner._ebean_getExtensionAccessors().getOffset(accessor);
+ }
+
+ protected EntityBean getExtensionBean(ExtensionAccessor accessor) {
+ return owner._ebean_getExtension(accessor);
+ }
+
+ @Override
+ public int findProperty(String propertyName) {
+ String[] names = owner._ebean_getPropertyNames();
+ int i;
+ for (i = 0; i < names.length; i++) {
+ if (names[i].equals(propertyName)) {
+ return i;
+ }
+ }
+ for (ExtensionAccessor acc : owner._ebean_getExtensionAccessors()) {
+ names = acc.getProperties();
+ for (int j = 0; j < names.length; j++) {
+ if (names[j].equals(propertyName)) {
+ return i;
+ }
+ i++;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public String property(int propertyIndex) {
+ if (propertyIndex == -1) {
+ return null;
+ }
+ ExtensionAccessor accessor = findAccessor(propertyIndex);
+ if (accessor == null) {
+ return owner._ebean_getPropertyName(propertyIndex);
+ } else {
+ int offset = getOffset(accessor);
+ return getExtensionBean(accessor)._ebean_getPropertyName(propertyIndex - offset);
+ }
+ }
+
+ @Override
+ public int propertyLength() {
+ return owner._ebean_getPropertyNames().length
+ + owner._ebean_getExtensionAccessors().getPropertyLength();
+ }
+
+ @Override
+ public Object value(int index) {
+ ExtensionAccessor accessor = findAccessor(index);
+ if (accessor == null) {
+ return owner._ebean_getField(index);
+ } else {
+ int offset = getOffset(accessor);
+ return getExtensionBean(accessor)._ebean_getField(index - offset);
+ }
+ }
+
+ @Override
+ public Object valueIntercept(int index) {
+ ExtensionAccessor accessor = findAccessor(index);
+ if (accessor == null) {
+ return owner._ebean_getFieldIntercept(index);
+ } else {
+ int offset = getOffset(accessor);
+ return getExtensionBean(accessor)._ebean_getFieldIntercept(index - offset);
+ }
+ }
+
+ @Override
+ public void setValue(int index, Object value) {
+ ExtensionAccessor accessor = findAccessor(index);
+ if (accessor == null) {
+ owner._ebean_setField(index, value);
+ } else {
+ int offset = getOffset(accessor);
+ getExtensionBean(accessor)._ebean_setField(index - offset, value);
+ }
+ }
+
+ @Override
+ public void setValueIntercept(int index, Object value) {
+ ExtensionAccessor accessor = findAccessor(index);
+ if (accessor == null) {
+ owner._ebean_setFieldIntercept(index, value);
+ } else {
+ int offset = getOffset(accessor);
+ getExtensionBean(accessor)._ebean_setFieldIntercept(index - offset, value);
+ }
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java b/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java
index 26f5faf8d5..ee0809515f 100644
--- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java
+++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java
@@ -13,15 +13,14 @@
* required for updates such as per property changed, loaded, dirty state, original values
* bean state etc.
*/
-public class InterceptReadOnly implements EntityBeanIntercept {
+public class InterceptReadOnly extends InterceptBase implements EntityBeanIntercept {
- private final EntityBean owner;
/**
* Create with a given entity.
*/
public InterceptReadOnly(Object ownerBean) {
- this.owner = (EntityBean) ownerBean;
+ super((EntityBean) ownerBean);
}
@Override
@@ -234,22 +233,7 @@ public Object origValue(int propertyIndex) {
return null;
}
- @Override
- public int findProperty(String propertyName) {
- return 0;
- }
-
- @Override
- public String property(int propertyIndex) {
- return null;
- }
-
- @Override
- public int propertyLength() {
- return 0;
- }
-
- @Override
+ @Override
public void setPropertyLoaded(String propertyName, boolean loaded) {
}
diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java
index c760ca10b4..b08d310683 100644
--- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java
+++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java
@@ -12,7 +12,12 @@
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URL;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -22,9 +27,8 @@
* This provides the mechanisms to support deferred fetching of reference beans
* and oldValues generation for concurrency checking.
*/
-public final class InterceptReadWrite implements EntityBeanIntercept {
-
- private static final long serialVersionUID = -3664031775464862649L;
+public final class InterceptReadWrite extends InterceptBase implements EntityBeanIntercept {
+ private static final long serialVersionUID = 1834735632647183821L;
private static final int STATE_NEW = 0;
private static final int STATE_REFERENCE = 1;
@@ -55,11 +59,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
private String ebeanServerName;
private boolean deletedFromCollection;
-
- /**
- * The actual entity bean that 'owns' this intercept.
- */
- private final EntityBean owner;
private EntityBean embeddedOwner;
private int embeddedOwnerIndex;
/**
@@ -101,15 +100,15 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
* Create with a given entity.
*/
public InterceptReadWrite(Object ownerBean) {
- this.owner = (EntityBean) ownerBean;
- this.flags = new byte[owner._ebean_getPropertyNames().length];
+ super((EntityBean) ownerBean);
+ this.flags = new byte[super.propertyLength()];
}
/**
* EXPERIMENTAL - Constructor only for use by serialization frameworks.
*/
public InterceptReadWrite() {
- this.owner = null;
+ super(null);
this.flags = null;
}
@@ -227,7 +226,7 @@ public boolean isDirty() {
}
if (mutableInfo != null) {
for (int i = 0; i < mutableInfo.length; i++) {
- if (mutableInfo[i] != null && !mutableInfo[i].isEqualToObject(owner._ebean_getField(i))) {
+ if (mutableInfo[i] != null && !mutableInfo[i].isEqualToObject(value(i))) {
dirty = true;
break;
}
@@ -411,25 +410,6 @@ public Object origValue(int propertyIndex) {
return origValues[propertyIndex];
}
- @Override
- public int findProperty(String propertyName) {
- final String[] names = owner._ebean_getPropertyNames();
- for (int i = 0; i < names.length; i++) {
- if (names[i].equals(propertyName)) {
- return i;
- }
- }
- return -1;
- }
-
- @Override
- public String property(int propertyIndex) {
- if (propertyIndex == -1) {
- return null;
- }
- return owner._ebean_getPropertyName(propertyIndex);
- }
-
@Override
public int propertyLength() {
return flags.length;
@@ -571,7 +551,7 @@ public void addDirtyPropertyNames(Set props, String prefix) {
props.add((prefix == null ? property(i) : prefix + property(i)));
} else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) {
// an embedded property has been changed - recurse
- final EntityBean embeddedBean = (EntityBean) owner._ebean_getField(i);
+ final EntityBean embeddedBean = (EntityBean) value(i);
embeddedBean._ebean_getIntercept().addDirtyPropertyNames(props, property(i) + ".");
}
}
@@ -579,9 +559,10 @@ public void addDirtyPropertyNames(Set props, String prefix) {
@Override
public boolean hasDirtyProperty(Set propertyNames) {
- final String[] names = owner._ebean_getPropertyNames();
+ String[] names = owner._ebean_getPropertyNames();
final int len = propertyLength();
- for (int i = 0; i < len; i++) {
+ int i;
+ for (i = 0; i < len; i++) {
if (isChangedProp(i)) {
if (propertyNames.contains(names[i])) {
return true;
@@ -592,6 +573,22 @@ public boolean hasDirtyProperty(Set propertyNames) {
}
}
}
+ for (ExtensionAccessor acc : owner._ebean_getExtensionAccessors()) {
+ names = acc.getProperties();
+ for (int j = 0; j < names.length; j++) {
+ if (isChangedProp(i)) {
+ if (propertyNames.contains(names[j])) {
+ return true;
+ }
+ } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) {
+ if (propertyNames.contains(names[j])) {
+ return true;
+ }
+ }
+ i++;
+ }
+ }
+
return false;
}
@@ -609,7 +606,7 @@ public void addDirtyPropertyValues(Map dirtyValues, String pr
if (isChangedProp(i)) {
// the property has been changed on this bean
final String propName = (prefix == null ? property(i) : prefix + property(i));
- final Object newVal = owner._ebean_getField(i);
+ final Object newVal = value(i);
final Object oldVal = origValue(i);
if (notEqual(oldVal, newVal)) {
dirtyValues.put(propName, new ValuePair(newVal, oldVal));
@@ -628,7 +625,7 @@ public void addDirtyPropertyValues(BeanDiffVisitor visitor) {
for (int i = 0; i < len; i++) {
if (isChangedProp(i)) {
// the property has been changed on this bean
- final Object newVal = owner._ebean_getField(i);
+ final Object newVal = value(i);
final Object oldVal = origValue(i);
if (notEqual(oldVal, newVal)) {
visitor.visit(i, newVal, oldVal);
@@ -1036,7 +1033,7 @@ public Map loadErrors() {
public boolean isChangedProp(int i) {
if ((flags[i] & FLAG_CHANGED_PROP) != 0) {
return true;
- } else if (mutableInfo == null || mutableInfo[i] == null || mutableInfo[i].isEqualToObject(owner._ebean_getField(i))) {
+ } else if (mutableInfo == null || mutableInfo[i] == null || mutableInfo[i].isEqualToObject(value(i))) {
return false;
} else {
// mark for change
diff --git a/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java
new file mode 100644
index 0000000000..c62d099a4e
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java
@@ -0,0 +1,70 @@
+package io.ebean.bean.extend;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Annotation for a class to extend an existing class.
+ *
+ * Normally, you would have annotated like the following example
+ *
+ *
+ * package basePkg;
+ * import extPkg.MyExtEntity;
+ * class MyBaseEntity {
+ * // this line is mandatory, to allow deletion of MyBaseEntity
+ * @OneToOne(optional = true, cascade = Cascade.ALL)
+ * private MyExtEntity
+ * }
+ *
+ * package extPkg;
+ * import basePkg.myBaseEntity;
+ * class MyExtEntity {
+ * @OneToOne(optional = false)
+ * private MyBaseEntity
+ * }
+ *
+ *
+ * If you spread your code over different packages or (especially in different maven modules), you'll get problems, because you'll get cyclic depencencies.
+ *
+ * To break up these dependencies, you can annotate 'MyExtEntity'
+ *
+ *
+ * package extPkg;
+ * import basePkg.myBaseEntity;
+ * @EntityExtension(MyBaseEntity.class)
+ * class MyExtEntity {
+ * @OneToOne(optional = false)
+ * private MyBaseEntity
+ *
+ * private String someField;
+ * }
+ *
+ * This will create a virtual property in the MyBaseEntity without adding a dependency to MyExtEntity.
+ *
+ * You may add a
+ *
+ * public static MyExtEntity get(MyBaseEntity base) {
+ * throw new NotEnhancedException();
+ * }
+ *
+ * This getter will be replaced by the enhancer, so that you can easily get it with
+ * MyExtEntiy.get(base).getSomeField()
.
+ *
+ * Technically, the instance of MyExtEntiy is stored in the _ebean_extension_storage
array of
+ * MyBaseEntity
.
+ *
+ * If you save the MyBaseEntity
, it will also save the data stored in MyExtEntity
.
+ *
+ * @author Alexander Wagner, FOCONIS AG
+ */
+@Documented
+@Target(TYPE)
+@Retention(RUNTIME)
+public @interface EntityExtension {
+ Class extends ExtendableBean>[] value();
+}
diff --git a/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java
new file mode 100644
index 0000000000..cb491c3508
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java
@@ -0,0 +1,10 @@
+package io.ebean.bean.extend;
+
+/**
+ * Marker for EntityExtension superclass.
+ *
+ * @author Noemi Praml, FOCONIS AG
+ */
+public @interface EntityExtensionSuperclass {
+
+}
diff --git a/ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java b/ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java
new file mode 100644
index 0000000000..1f4f4b7de1
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java
@@ -0,0 +1,20 @@
+package io.ebean.bean.extend;
+
+import io.ebean.bean.EntityBean;
+import io.ebean.bean.NotEnhancedException;
+
+/**
+ * Marker interface for beans that can be extended with @EntityExtension.
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public interface ExtendableBean {
+
+ /**
+ * Returns an array of registered extensions. This may be useful for bean validation.
+ * NOTE: The passed array should NOT be modified.
+ */
+ default EntityBean[] _ebean_getExtensions() {
+ throw new NotEnhancedException();
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/common/BeanList.java b/ebean-api/src/main/java/io/ebean/common/BeanList.java
index 12d0770d62..8fa03659fb 100644
--- a/ebean-api/src/main/java/io/ebean/common/BeanList.java
+++ b/ebean-api/src/main/java/io/ebean/common/BeanList.java
@@ -8,14 +8,14 @@
/**
* List capable of lazy loading and modification awareness.
*/
-public final class BeanList extends AbstractBeanCollection implements List, BeanCollectionAdd {
+public class BeanList extends AbstractBeanCollection implements List, BeanCollectionAdd {
private static final long serialVersionUID = 1L;
/**
* The underlying List implementation.
*/
- private List list;
+ List list;
/**
* Specify the underlying List implementation.
@@ -111,30 +111,30 @@ public boolean checkEmptyLazyLoad() {
}
}
+ protected void initList(boolean skipLoad, boolean onlyIds) {
+ if (skipLoad) {
+ list = new ArrayList<>();
+ } else {
+ lazyLoadCollection(onlyIds);
+ }
+ }
+
private void initClear() {
lock.lock();
try {
if (list == null) {
- if (!disableLazyLoad && modifyListening) {
- lazyLoadCollection(true);
- } else {
- list = new ArrayList<>();
- }
+ initList(disableLazyLoad || !modifyListening, true);
}
} finally {
lock.unlock();
}
}
- private void init() {
+ void init() {
lock.lock();
try {
if (list == null) {
- if (disableLazyLoad) {
- list = new ArrayList<>();
- } else {
- lazyLoadCollection(false);
- }
+ initList(disableLazyLoad, false);
}
} finally {
lock.unlock();
@@ -164,7 +164,10 @@ public Collection actualDetails() {
}
@Override
- public Collection> actualEntries() {
+ public Collection> actualEntries(boolean load) {
+ if (load) {
+ init();
+ }
return list;
}
diff --git a/ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java b/ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java
new file mode 100644
index 0000000000..8ffa4b47df
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java
@@ -0,0 +1,133 @@
+package io.ebean.common;
+
+import io.ebean.bean.BeanCollection;
+import io.ebean.bean.BeanCollectionLoader;
+import io.ebean.bean.EntityBean;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * This bean list can perform additions without populating the list.
+ * This might be useful, if you just want to add entries to an existing collection.
+ * Works only for lists and only if there is no order column
+ */
+public class BeanListLazyAdd extends BeanList {
+
+ public BeanListLazyAdd(BeanCollectionLoader loader, EntityBean ownerBean, String propertyName) {
+ super(loader, ownerBean, propertyName);
+ }
+
+ private List lazyAddedEntries;
+
+ @Override
+ public boolean add(E bean) {
+ checkReadOnly();
+
+ lock.lock();
+ try {
+ if (list == null) {
+ // list is not yet initialized, so we may add elements to a spare list
+ if (lazyAddedEntries == null) {
+ lazyAddedEntries = new ArrayList<>();
+ }
+ lazyAddedEntries.add(bean);
+ } else {
+ list.add(bean);
+ }
+ } finally {
+ lock.unlock();
+ }
+
+ if (modifyListening) {
+ modifyAddition(bean);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean addAll(Collection extends E> beans) {
+ checkReadOnly();
+
+ lock.lock();
+ try {
+ if (list == null) {
+ // list is not yet initialized, so we may add elements to a spare list
+ if (lazyAddedEntries == null) {
+ lazyAddedEntries = new ArrayList<>();
+ }
+ lazyAddedEntries.addAll(beans);
+ } else {
+ list.addAll(beans);
+ }
+ } finally {
+ lock.unlock();
+ }
+
+ if (modifyListening) {
+ getModifyHolder().modifyAdditionAll(beans);
+ }
+
+ return true;
+ }
+
+
+ @Override
+ public void loadFrom(BeanCollection> other) {
+ super.loadFrom(other);
+ if (lazyAddedEntries != null) {
+ list.addAll(lazyAddedEntries);
+ lazyAddedEntries = null;
+ }
+ }
+
+ /**
+ * on init, this happens on all accessor methods except on 'add' and addAll,
+ * we add the lazy added entries at the end of the list
+ */
+ @Override
+ protected void initList(boolean skipLoad, boolean onlyIds) {
+ if (skipLoad) {
+ if (lazyAddedEntries != null) {
+ list = lazyAddedEntries;
+ lazyAddedEntries = null;
+ } else {
+ list = new ArrayList<>();
+ }
+ } else {
+ lazyLoadCollection(onlyIds);
+ if (lazyAddedEntries != null) {
+ list.addAll(lazyAddedEntries);
+ lazyAddedEntries = null;
+ }
+ }
+ }
+
+ @Override
+ public List getLazyAddedEntries(boolean reset) {
+ List ret = lazyAddedEntries;
+ if (reset) {
+ lazyAddedEntries = null;
+ }
+ return ret;
+ }
+
+ @Override
+ public boolean isSkipSave() {
+ return lazyAddedEntries == null && super.isSkipSave();
+ }
+
+ public boolean checkEmptyLazyLoad() {
+ if (list != null) {
+ return false;
+ } else if (lazyAddedEntries == null) {
+ list = new ArrayList<>();
+ return true;
+ } else {
+ list = lazyAddedEntries;
+ lazyAddedEntries = null;
+ return false;
+ }
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/common/BeanMap.java b/ebean-api/src/main/java/io/ebean/common/BeanMap.java
index fe959c22c0..d1c5801e38 100644
--- a/ebean-api/src/main/java/io/ebean/common/BeanMap.java
+++ b/ebean-api/src/main/java/io/ebean/common/BeanMap.java
@@ -187,7 +187,10 @@ public Collection actualDetails() {
* Returns the map entrySet.
*/
@Override
- public Collection> actualEntries() {
+ public Collection> actualEntries(boolean load) {
+ if (load) {
+ init();
+ }
return map.entrySet();
}
diff --git a/ebean-api/src/main/java/io/ebean/common/BeanSet.java b/ebean-api/src/main/java/io/ebean/common/BeanSet.java
index 81d72284b8..05451657f4 100644
--- a/ebean-api/src/main/java/io/ebean/common/BeanSet.java
+++ b/ebean-api/src/main/java/io/ebean/common/BeanSet.java
@@ -165,7 +165,10 @@ public Collection actualDetails() {
}
@Override
- public Collection> actualEntries() {
+ public Collection> actualEntries(boolean load) {
+ if (load) {
+ init();
+ }
return set;
}
diff --git a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java
index 8c25f20143..be74062651 100644
--- a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java
+++ b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java
@@ -17,6 +17,7 @@
import io.ebean.event.readaudit.ReadAuditLogger;
import io.ebean.event.readaudit.ReadAuditPrepare;
import io.ebean.meta.MetricNamingMatch;
+import io.ebean.plugin.CustomDeployParser;
import io.ebean.util.StringHelper;
import javax.persistence.EnumType;
@@ -397,6 +398,8 @@ public class DatabaseConfig {
*/
private Clock clock = Clock.systemUTC();
+ private TempFileProvider tempFileProvider = new WeakRefTempFileProvider();
+
private List idGenerators = new ArrayList<>();
private List findControllers = new ArrayList<>();
private List persistControllers = new ArrayList<>();
@@ -406,6 +409,7 @@ public class DatabaseConfig {
private List queryAdapters = new ArrayList<>();
private final List bulkTableEventListeners = new ArrayList<>();
private final List configStartupListeners = new ArrayList<>();
+ private final List customDeployParsers = new ArrayList<>();
/**
* By default inserts are included in the change log.
@@ -441,6 +445,8 @@ public class DatabaseConfig {
private int backgroundExecutorShutdownSecs = 30;
private BackgroundExecutorWrapper backgroundExecutorWrapper = new MdcBackgroundExecutorWrapper();
+ private boolean tenantPartitionedCache;
+
// defaults for the L2 bean caching
private int cacheMaxSize = 10000;
@@ -500,6 +506,11 @@ public class DatabaseConfig {
*/
private boolean queryPlanEnable;
+ /**
+ * Additional platform specific options for query-plan generation.
+ */
+ private String queryPlanOptions;
+
/**
* The default threshold in micros for collecting query plans.
*/
@@ -536,6 +547,10 @@ public class DatabaseConfig {
*/
private List mappingLocations = new ArrayList<>();
+ /**
+ * The maximum string size in clob fields.
+ */
+ private int maxStringSize = 0;
/**
* When true we do not need explicit GeneratedValue mapping.
*/
@@ -567,6 +582,14 @@ public void setClock(final Clock clock) {
this.clock = clock;
}
+ public TempFileProvider getTempFileProvider() {
+ return tempFileProvider;
+ }
+
+ public void setTempFileProvider(final TempFileProvider tempFileProvider) {
+ this.tempFileProvider = tempFileProvider;
+ }
+
/**
* Return the slow query time in millis.
*/
@@ -1491,6 +1514,21 @@ public void setBackgroundExecutorWrapper(BackgroundExecutorWrapper backgroundExe
this.backgroundExecutorWrapper = backgroundExecutorWrapper;
}
+ /**
+ * Returns, if the caches are partitioned by tenant.
+ */
+ public boolean isTenantPartitionedCache() {
+ return tenantPartitionedCache;
+ }
+
+ /**
+ * Sets the tenant partitioning mode for caches. This means, caches are created on demand,
+ * but they may not get invalidated across tenant boundaries *
+ */
+ public void setTenantPartitionedCache(boolean tenantPartitionedCache) {
+ this.tenantPartitionedCache = tenantPartitionedCache;
+ }
+
/**
* Return the L2 cache default max size.
*/
@@ -2714,6 +2752,17 @@ public List getServerConfigStartupListeners() {
return configStartupListeners;
}
+ /**
+ * Add a CustomDeployParser.
+ */
+ public void addCustomDeployParser(CustomDeployParser customDeployParser) {
+ customDeployParsers.add(customDeployParser);
+ }
+
+ public List getCustomDeployParsers() {
+ return customDeployParsers;
+ }
+
/**
* Register all the BeanPersistListener instances.
*
@@ -2884,6 +2933,7 @@ protected void loadSettings(PropertiesWrapper p) {
queryPlanTTLSeconds = p.getInt("queryPlanTTLSeconds", queryPlanTTLSeconds);
slowQueryMillis = p.getLong("slowQueryMillis", slowQueryMillis);
queryPlanEnable = p.getBoolean("queryPlan.enable", queryPlanEnable);
+ queryPlanOptions = p.get("queryPlan.options", queryPlanOptions);
queryPlanThresholdMicros = p.getLong("queryPlan.thresholdMicros", queryPlanThresholdMicros);
queryPlanCapture = p.getBoolean("queryPlan.capture", queryPlanCapture);
queryPlanCapturePeriodSecs = p.getLong("queryPlan.capturePeriodSecs", queryPlanCapturePeriodSecs);
@@ -2898,6 +2948,7 @@ protected void loadSettings(PropertiesWrapper p) {
useValidationNotNull = p.getBoolean("useValidationNotNull", useValidationNotNull);
autoReadOnlyDataSource = p.getBoolean("autoReadOnlyDataSource", autoReadOnlyDataSource);
idGeneratorAutomatic = p.getBoolean("idGeneratorAutomatic", idGeneratorAutomatic);
+ maxStringSize = p.getInt("maxStringSize", maxStringSize);
backgroundExecutorSchedulePoolSize = p.getInt("backgroundExecutorSchedulePoolSize", backgroundExecutorSchedulePoolSize);
backgroundExecutorShutdownSecs = p.getInt("backgroundExecutorShutdownSecs", backgroundExecutorShutdownSecs);
@@ -2973,6 +3024,15 @@ protected void loadSettings(PropertiesWrapper p) {
ddlPlaceholders = p.get("ddl.placeholders", ddlPlaceholders);
ddlHeader = p.get("ddl.header", ddlHeader);
+ tenantPartitionedCache = p.getBoolean("tenantPartitionedCache", tenantPartitionedCache);
+
+ cacheMaxIdleTime = p.getInt("cacheMaxIdleTime", cacheMaxIdleTime);
+ cacheMaxSize = p.getInt("cacheMaxSize", cacheMaxSize);
+ cacheMaxTimeToLive = p.getInt("cacheMaxTimeToLive", cacheMaxTimeToLive);
+ queryCacheMaxIdleTime = p.getInt("queryCacheMaxIdleTime", queryCacheMaxIdleTime);
+ queryCacheMaxSize = p.getInt("queryCacheMaxSize", queryCacheMaxSize);
+ queryCacheMaxTimeToLive = p.getInt("queryCacheMaxTimeToLive", queryCacheMaxTimeToLive);
+
// read tenant-configuration from config:
// tenant.mode = NONE | DB | SCHEMA | CATALOG | PARTITION
String mode = p.get("tenant.mode");
@@ -3261,6 +3321,21 @@ public void setIdGeneratorAutomatic(boolean idGeneratorAutomatic) {
this.idGeneratorAutomatic = idGeneratorAutomatic;
}
+ /**
+ * When maxStringSize is set, the string length in binds is limited to this size. If the string exceeds the given size,
+ * a Persistence exception is thrown. This is to handle DoS attacks or discover programming errors.
+ */
+ public int getMaxStringSize() {
+ return maxStringSize;
+ }
+
+ /**
+ * Setst the maximum size a string can have in bind arguments. See {@link #getMaxStringSize()}
+ */
+ public void setMaxStringSize(int maxStringSize) {
+ this.maxStringSize = maxStringSize;
+ }
+
/**
* Return true if query plan capture is enabled.
*/
@@ -3275,6 +3350,20 @@ public void setQueryPlanEnable(boolean queryPlanEnable) {
this.queryPlanEnable = queryPlanEnable;
}
+ /**
+ * Returns platform specific query plan options.
+ */
+ public String getQueryPlanOptions() {
+ return queryPlanOptions;
+ }
+
+ /**
+ * Set platform specific query plan options.
+ */
+ public void setQueryPlanOptions(String queryPlanOptions) {
+ this.queryPlanOptions = queryPlanOptions;
+ }
+
/**
* Return the query plan collection threshold in microseconds.
*/
diff --git a/ebean-api/src/main/java/io/ebean/config/DeleteOnShutdownTempFileProvider.java b/ebean-api/src/main/java/io/ebean/config/DeleteOnShutdownTempFileProvider.java
new file mode 100644
index 0000000000..ad257c98ee
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/config/DeleteOnShutdownTempFileProvider.java
@@ -0,0 +1,68 @@
+package io.ebean.config;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TempFileProvider implementation, which deletes all temp files on shutdown.
+ *
+ * @author Roland Praml, FOCONIS AG
+ *
+ */
+public class DeleteOnShutdownTempFileProvider implements TempFileProvider {
+
+ private static final Logger logger = LoggerFactory.getLogger(DeleteOnShutdownTempFileProvider.class);
+
+ List tempFiles = new ArrayList<>();
+ private final String prefix;
+ private final String suffix;
+ private final File directory;
+
+ /**
+ * Creates the TempFileProvider with default prefix "db-".
+ */
+ public DeleteOnShutdownTempFileProvider() {
+ this("db-", null, null);
+ }
+
+ /**
+ * Creates the TempFileProvider.
+ */
+ public DeleteOnShutdownTempFileProvider(String prefix, String suffix, File directory) {
+ this.prefix = prefix;
+ this.suffix = suffix;
+ this.directory = directory;
+ }
+
+ @Override
+ public File createTempFile() throws IOException {
+ File file = File.createTempFile(prefix, suffix, directory);
+ synchronized (tempFiles) {
+ tempFiles.add(file.getAbsolutePath());
+ }
+ return file;
+ }
+
+ /**
+ * Deletes all created files on shutdown.
+ */
+ @Override
+ public void shutdown() {
+ synchronized (tempFiles) {
+ for (String path : tempFiles) {
+ if (new File(path).delete()) {
+ logger.trace("deleted {}", path);
+ } else {
+ logger.warn("could not delete {}", path);
+ }
+ }
+ tempFiles.clear();
+ }
+ }
+
+}
diff --git a/ebean-api/src/main/java/io/ebean/config/TempFileProvider.java b/ebean-api/src/main/java/io/ebean/config/TempFileProvider.java
new file mode 100644
index 0000000000..4658b46c28
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/config/TempFileProvider.java
@@ -0,0 +1,23 @@
+package io.ebean.config;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Creates a temp file for the ScalarTypeFile datatype.
+ *
+ * @author Roland Praml, FOCONIS AG
+ *
+ */
+public interface TempFileProvider {
+
+ /**
+ * Creates a tempFile.
+ */
+ File createTempFile() throws IOException;
+
+ /**
+ * Shutdown the tempFileProvider.
+ */
+ void shutdown();
+}
diff --git a/ebean-api/src/main/java/io/ebean/config/WeakRefTempFileProvider.java b/ebean-api/src/main/java/io/ebean/config/WeakRefTempFileProvider.java
new file mode 100644
index 0000000000..aa3ae82905
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/config/WeakRefTempFileProvider.java
@@ -0,0 +1,145 @@
+package io.ebean.config;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * WeakRefTempFileProvider will delete the tempFile if all references to the returned File
+ * object are collected by the garbage collection.
+ *
+ * @author Roland Praml, FOCONIS AG
+ *
+ */
+public class WeakRefTempFileProvider implements TempFileProvider {
+
+ private static final Logger logger = LoggerFactory.getLogger(WeakRefTempFileProvider.class);
+
+ private final ReferenceQueue tempFiles = new ReferenceQueue<>();
+
+ private WeakFileReference root;
+
+ private final String prefix;
+ private final String suffix;
+ private final File directory;
+
+ /**
+ * We hold a linkedList of weak references. So we can remove stale files in O(1)
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+ private static class WeakFileReference extends WeakReference {
+
+ String path;
+ WeakFileReference prev;
+ WeakFileReference next;
+
+ WeakFileReference(File referent, ReferenceQueue super File> q) {
+ super(referent, q);
+ path = referent.getAbsolutePath();
+ }
+
+ boolean delete(boolean shutdown) {
+ File file = new File(path);
+ if (!file.exists()) {
+ logger.trace("already deleted {}", path);
+ return true;
+ } else if (file.delete()) {
+ logger.trace("deleted {}", path);
+ return true;
+ } else {
+ if (shutdown) {
+ logger.warn("could not delete {}", path);
+ } else {
+ logger.info("could not delete {} - will delete on shutdown", path);
+ }
+ return false;
+ }
+ }
+ }
+
+
+ /**
+ * Creates the TempFileProvider with default prefix "db-".
+ */
+ public WeakRefTempFileProvider() {
+ this("db-", null, null);
+ }
+
+ /**
+ * Creates the TempFileProvider.
+ */
+ public WeakRefTempFileProvider(String prefix, String suffix, File directory) {
+ this.prefix = prefix;
+ this.suffix = suffix;
+ this.directory = directory;
+ }
+
+ @Override
+ public File createTempFile() throws IOException {
+ File tempFile = File.createTempFile(prefix, suffix, directory);
+ logger.trace("createTempFile: {}", tempFile);
+ synchronized (this) {
+ add(new WeakFileReference(tempFile, tempFiles));
+ }
+ return tempFile;
+ }
+
+ /**
+ * Will delete stale files.
+ * This is public to use in tests.
+ */
+ public void deleteStaleTempFiles() {
+ synchronized (this) {
+ deleteStaleTempFilesInternal();
+ }
+ }
+
+ private void deleteStaleTempFilesInternal() {
+ WeakFileReference ref;
+ while ((ref = (WeakFileReference) tempFiles.poll()) != null) {
+ if (ref.delete(false)) {
+ remove(ref); // remove from linkedList only, if delete was successful.
+ }
+ }
+ }
+
+ private void add(WeakFileReference ref) {
+ deleteStaleTempFilesInternal();
+
+ if (root == null) {
+ root = ref;
+ } else {
+ ref.next = root;
+ root.prev = ref;
+ root = ref;
+ }
+ }
+
+ private void remove(WeakFileReference ref) {
+ if (ref.next != null) {
+ ref.next.prev = ref.prev;
+ }
+ if (ref.prev != null) {
+ ref.prev.next = ref.next;
+ } else {
+ root = ref.next;
+ }
+ }
+
+ /**
+ * Deletes all created files on shutdown.
+ */
+ @Override
+ public void shutdown() {
+ while (root != null) {
+ root.delete(true);
+ root = root.next;
+ }
+ }
+
+}
diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java
index 7de8de1425..7408147018 100644
--- a/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java
+++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java
@@ -673,7 +673,7 @@ public String fromForUpdate(Query.LockWait lockWait) {
protected String withForUpdate(String sql, Query.LockWait lockWait, Query.LockType lockType) {
// silently assume the database does not support the "for update" clause.
- log.log(INFO, "it seems your database does not support the 'for update' clause");
+ log.log(INFO, "it seems your database does not support the ''for update'' clause");
return sql;
}
diff --git a/ebean-api/src/main/java/io/ebean/meta/MetaInfoManager.java b/ebean-api/src/main/java/io/ebean/meta/MetaInfoManager.java
index 81e7944f04..7f61640bff 100644
--- a/ebean-api/src/main/java/io/ebean/meta/MetaInfoManager.java
+++ b/ebean-api/src/main/java/io/ebean/meta/MetaInfoManager.java
@@ -64,5 +64,9 @@ default List collectMetricsAsData() {
*/
List queryPlanCollectNow(QueryPlanRequest request);
+ /**
+ * Creates a new MetricReportGenerator. This can be used to embed in your web-application
+ */
+ MetricReportGenerator createReportGenerator();
}
diff --git a/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java b/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java
index c1ae44a823..4258b7c9ca 100644
--- a/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java
+++ b/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java
@@ -2,6 +2,8 @@
import io.ebean.ProfileLocation;
+import java.time.Instant;
+
/**
* Meta data for captured query plan.
*/
@@ -42,6 +44,11 @@ public interface MetaQueryPlan {
*/
String plan();
+ /**
+ * The tenant ID of the plan.
+ */
+ Object tenantId();
+
/**
* Return the query execution time associated with the bind values capture.
*/
@@ -51,4 +58,14 @@ public interface MetaQueryPlan {
* Return the total count of times bind capture has occurred.
*/
long captureCount();
+
+ /**
+ * Return the time taken to capture this plan in microseconds.
+ */
+ long captureMicros();
+
+ /**
+ * Return the instant when the bind values were captured.
+ */
+ Instant whenCaptured();
}
diff --git a/ebean-api/src/main/java/io/ebean/meta/MetricReportGenerator.java b/ebean-api/src/main/java/io/ebean/meta/MetricReportGenerator.java
new file mode 100644
index 0000000000..054b77a56d
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/meta/MetricReportGenerator.java
@@ -0,0 +1,22 @@
+package io.ebean.meta;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * The metric report. most likely a HTML report.
+ */
+public interface MetricReportGenerator {
+
+ /**
+ * Writes the report to the outputStream. The stream will not be closed.
+ */
+ void writeReport(OutputStream out) throws IOException;
+
+ /**
+ * Used to configure the metric. This report dependent.
+ * Returns a string result, that should be sent back to the web application
+ */
+ String configure(List values);
+}
diff --git a/ebean-api/src/main/java/io/ebean/meta/MetricReportValue.java b/ebean-api/src/main/java/io/ebean/meta/MetricReportValue.java
new file mode 100644
index 0000000000..4ab5a2c9d0
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/meta/MetricReportValue.java
@@ -0,0 +1,39 @@
+package io.ebean.meta;
+
+/**
+ * A Metric report value used to configure a metric report.
+ * This is most likely a REST call from the provided web-application
+ */
+public class MetricReportValue {
+ private String name;
+ private String value;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public int intValue() {
+ return Integer.valueOf(value);
+ }
+
+ public MetricReportValue() {
+
+ }
+
+ public MetricReportValue(String name, Object value) {
+ this.name = name;
+ this.value = String.valueOf(value);
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/metric/CountMetric.java b/ebean-api/src/main/java/io/ebean/metric/CountMetric.java
index 3ce785c1ea..d125134552 100644
--- a/ebean-api/src/main/java/io/ebean/metric/CountMetric.java
+++ b/ebean-api/src/main/java/io/ebean/metric/CountMetric.java
@@ -5,7 +5,7 @@
/**
* Metric for timed events like transaction execution times.
*/
-public interface CountMetric {
+public interface CountMetric extends Metric {
/**
* Add to the counter.
@@ -32,8 +32,4 @@ public interface CountMetric {
*/
void reset();
- /**
- * Visit non empty metrics.
- */
- void visit(MetricVisitor visitor);
}
diff --git a/ebean-api/src/main/java/io/ebean/metric/Metric.java b/ebean-api/src/main/java/io/ebean/metric/Metric.java
new file mode 100644
index 0000000000..374b2134e2
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/metric/Metric.java
@@ -0,0 +1,13 @@
+package io.ebean.metric;
+
+import io.ebean.meta.MetricVisitor;
+
+/**
+ * Base for all metrics.
+ */
+public interface Metric {
+ /**
+ * Visit the underlying metric.
+ */
+ void visit(MetricVisitor visitor);
+}
diff --git a/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java b/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java
index d5fb8bd282..478d0a9933 100644
--- a/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java
+++ b/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java
@@ -2,6 +2,9 @@
import io.ebean.ProfileLocation;
+import java.util.function.IntSupplier;
+import java.util.function.LongSupplier;
+
/**
* Factory to create timed metric counters.
*/
@@ -29,6 +32,16 @@ static MetricFactory get() {
*/
CountMetric createCountMetric(String name);
+ /**
+ * Create a metric, that gets the value from a supplier.
+ */
+ Metric createMetric(String name, LongSupplier supplier);
+
+ /**
+ * Create a metric, that gets the value from a supplier.
+ */
+ Metric createMetric(String name, IntSupplier supplier);
+
/**
* Create a Timed metric.
*/
diff --git a/ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java b/ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java
new file mode 100644
index 0000000000..d881320247
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java
@@ -0,0 +1,31 @@
+package io.ebean.metric;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Global registry of custom metrics instances created.
+ */
+public final class MetricRegistry {
+ public interface RegistryEntry {
+ void remove();
+ }
+
+ private static final List list = Collections.synchronizedList(new ArrayList<>());
+
+ /**
+ * Register the metric instance.
+ */
+ public static RegistryEntry register(Metric location) {
+ list.add(location);
+ return () -> list.remove(location);
+ }
+
+ /**
+ * Return all the registered extra metrics.
+ */
+ public static List registered() {
+ return list;
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java b/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java
index 24999ce0ed..c1f24eea45 100644
--- a/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java
+++ b/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java
@@ -5,15 +5,11 @@
/**
* Internal Query plan metric holder.
*/
-public interface QueryPlanMetric {
+public interface QueryPlanMetric extends Metric {
/**
* Return the underlying timed metric.
*/
TimedMetric metric();
- /**
- * Visit the underlying metric.
- */
- void visit(MetricVisitor visitor);
}
diff --git a/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java b/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java
index 17c3defb95..4ec95581a4 100644
--- a/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java
+++ b/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java
@@ -5,7 +5,7 @@
/**
* Metric for timed events like transaction execution times.
*/
-public interface TimedMetric {
+public interface TimedMetric extends Metric {
/**
* Add a time event (usually in microseconds).
@@ -37,8 +37,4 @@ public interface TimedMetric {
*/
TimedMetricStats collect(boolean reset);
- /**
- * Visit non empty metrics.
- */
- void visit(MetricVisitor visitor);
}
diff --git a/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java b/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java
index 5d10397ba2..aeff635e82 100644
--- a/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java
+++ b/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java
@@ -5,7 +5,7 @@
/**
* A map of timed metrics keyed by a string.
*/
-public interface TimedMetricMap {
+public interface TimedMetricMap extends Metric {
/**
* Add a time event given the start nanos.
@@ -17,8 +17,4 @@ public interface TimedMetricMap {
*/
void add(String key, long exeMicros);
- /**
- * Visit the metric.
- */
- void visit(MetricVisitor visitor);
}
diff --git a/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java b/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java
new file mode 100644
index 0000000000..4c854f2f1e
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java
@@ -0,0 +1,15 @@
+package io.ebean.plugin;
+
+import io.ebean.config.dbplatform.DatabasePlatform;
+
+/**
+ * Fired after all beans are parsed. You may implement own parsers to handle custom annotations.
+ * (See test case for example)
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+@FunctionalInterface
+public interface CustomDeployParser {
+
+ void parse(DeployBeanDescriptorMeta descriptor, DatabasePlatform databasePlatform);
+}
diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java
new file mode 100644
index 0000000000..4347a5057c
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java
@@ -0,0 +1,36 @@
+package io.ebean.plugin;
+
+import java.util.Collection;
+
+/**
+ * General deployment information. This is used in {@link CustomDeployParser}.
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public interface DeployBeanDescriptorMeta {
+
+ /**
+ * Return a collection of all BeanProperty deployment information.
+ */
+ public Collection extends DeployBeanPropertyMeta> propertiesAll();
+
+ /**
+ * Get a BeanProperty by its name.
+ */
+ public DeployBeanPropertyMeta getBeanProperty(String secondaryBeanName);
+
+ /**
+ * Return the DeployBeanDescriptorMeta for the given bean class.
+ */
+ public DeployBeanDescriptorMeta getDeployBeanDescriptorMeta(Class> propertyType);
+
+ /**
+ * Returns the discriminator column, if any.
+ * @return
+ */
+ public String getDiscriminatorColumn();
+
+ public String getBaseTable();
+
+ DeployBeanPropertyMeta idProperty();
+}
diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java
new file mode 100644
index 0000000000..d7d1d637a3
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java
@@ -0,0 +1,23 @@
+package io.ebean.plugin;
+
+public interface DeployBeanPropertyAssocMeta extends DeployBeanPropertyMeta {
+
+ /**
+ * Return the mappedBy deployment attribute.
+ *
+ * This is the name of the property in the 'detail' bean that maps back to
+ * this 'master' bean.
+ *
+ */
+ String getMappedBy();
+
+ /**
+ * Return the base table for this association.
+ *
+ * This has the table name which is used to determine the relationship for
+ * this association.
+ *
+ */
+ String getBaseTable();
+
+}
diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java
new file mode 100644
index 0000000000..4ba2d3e2a1
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java
@@ -0,0 +1,37 @@
+package io.ebean.plugin;
+
+import java.lang.reflect.Field;
+
+public interface DeployBeanPropertyMeta {
+
+ /**
+ * Return the name of the property.
+ */
+ String getName();
+
+ /**
+ * The database column name this is mapped to.
+ */
+ String getDbColumn();
+
+ /**
+ * Return the bean Field associated with this property.
+ */
+ Field getField();
+
+ /**
+ * The property is based on a formula.
+ */
+ void setSqlFormula(String sqlSelect, String sqlJoin);
+
+ /**
+ * Return the bean type.
+ */
+ Class> getOwningType();
+
+ /**
+ * Return the property type.
+ */
+ Class> getPropertyType();
+
+}
diff --git a/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java b/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java
index 935b788600..7acdb976f1 100644
--- a/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java
+++ b/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java
@@ -12,18 +12,11 @@
*/
public interface JsonBeanReader {
- /**
- * Read the JSON into given bean. Will update existing properties.
- */
- T read(T target);
-
/**
* Read the JSON returning a bean.
*/
- default T read() {
- return read(null);
- }
-
+ T read();
+
/**
* Create a new reader taking the context from the existing one but using a new JsonParser.
*/
diff --git a/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java b/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java
index d2c0df822b..ad91620ccb 100644
--- a/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java
+++ b/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java
@@ -2,8 +2,10 @@
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
+import io.ebean.BeanMergeOptions;
import io.ebean.FetchPath;
import io.ebean.plugin.BeanType;
+import io.ebean.plugin.Property;
import java.io.IOException;
import java.io.Reader;
@@ -64,7 +66,9 @@ public interface JsonContext {
* instances, so the object identity will not be preserved here.
*
* @throws JsonIOException When IOException occurs
+ * @deprecated use {@link #toBean(Class, JsonParser)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)}
*/
+ @Deprecated
void toBean(T target, JsonParser parser) throws JsonIOException;
/**
@@ -72,7 +76,9 @@ public interface JsonContext {
* See {@link #toBean(Class, JsonParser)} for details modified.
*
* @throws JsonIOException When IOException occurs
+ * @deprecated use {@link #toBean(Class, JsonParser, JsonReadOptions)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)}
*/
+ @Deprecated
void toBean(T target, JsonParser parser, JsonReadOptions options) throws JsonIOException;
/**
@@ -80,7 +86,9 @@ public interface JsonContext {
* See {@link #toBean(Class, JsonParser)} for details
*
* @throws JsonIOException When IOException occurs
+ * @deprecated use {@link #toBean(Class, Reader)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)}
*/
+ @Deprecated
void toBean(T target, Reader json) throws JsonIOException;
/**
@@ -88,7 +96,9 @@ public interface JsonContext {
* See {@link #toBean(Class, JsonParser)} for details modified.
*
* @throws JsonIOException When IOException occurs
+ * @deprecated use {@link #toBean(Class, Reader, JsonReadOptions)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)}
*/
+ @Deprecated
void toBean(T target, Reader json, JsonReadOptions options) throws JsonIOException;
/**
@@ -96,7 +106,9 @@ public interface JsonContext {
* See {@link #toBean(Class, JsonParser)} for details
*
* @throws JsonIOException When IOException occurs
+ * @deprecated use {@link #toBean(Class, String)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)}
*/
+ @Deprecated
void toBean(T target, String json) throws JsonIOException;
/**
@@ -104,9 +116,23 @@ public interface JsonContext {
* See {@link #toBean(Class, JsonParser)} for details
*
* @throws JsonIOException When IOException occurs
+ * @deprecated use {@link #toBean(Class, String, JsonReadOptions)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)}
*/
+ @Deprecated
void toBean(T target, String json, JsonReadOptions options) throws JsonIOException;
+ /**
+ * Read json parser input and returns the property value.
+ * This can be used to read a single property (e.g. ID property) from a JSON stream
+ */
+ T readProperty(Property property, JsonParser parser);
+
+ /**
+ * Read json parser input and returns the property value.
+ * See {@link #readProperty(Property, JsonParser)} for details
+ */
+ T readProperty(Property property, JsonParser parser, JsonReadOptions options);
+
/**
* Create and return a new bean reading for the bean type given the JSON options and source.
*
@@ -257,6 +283,16 @@ public interface JsonContext {
*/
String toJson(Object value, JsonWriteOptions options) throws JsonIOException;
+ /**
+ * Writes a single property of the current bean to generator.
+ */
+ void writeProperty(Property property, Object bean, JsonGenerator generator) throws JsonIOException;
+
+ /**
+ * Writes a single property of the current bean to generator.
+ */
+ void writeProperty(Property property, Object bean, JsonGenerator generator, JsonWriteOptions options) throws JsonIOException;
+
/**
* Return true if the type is known as an Entity bean or a List Set or
* Map of entity beans.
diff --git a/ebean-api/src/main/java/module-info.java b/ebean-api/src/main/java/module-info.java
index a8fb4ea387..cb7f9f7c7f 100644
--- a/ebean-api/src/main/java/module-info.java
+++ b/ebean-api/src/main/java/module-info.java
@@ -25,6 +25,7 @@
exports io.ebean;
exports io.ebean.bean;
+ exports io.ebean.bean.extend;
exports io.ebean.cache;
exports io.ebean.meta;
exports io.ebean.config;
@@ -41,5 +42,5 @@
exports io.ebean.text;
exports io.ebean.text.json;
exports io.ebean.util;
-
+ exports io.ebean.annotation.ext;
}
diff --git a/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java b/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java
index 5eb17bbe87..5b78dbbe15 100644
--- a/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java
+++ b/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java
@@ -9,9 +9,9 @@ class EbeanVersionTest {
@Test
void checkMinAgentVersion_ok() {
- assertFalse(EbeanVersion.checkMinAgentVersion("12.12.0"));
- assertFalse(EbeanVersion.checkMinAgentVersion("12.12.99"));
- assertFalse(EbeanVersion.checkMinAgentVersion("13.1.0"));
+ assertFalse(EbeanVersion.checkMinAgentVersion("13.10.0"));
+ assertFalse(EbeanVersion.checkMinAgentVersion("13.10.99"));
+ assertFalse(EbeanVersion.checkMinAgentVersion("14.1.0"));
}
@Test
diff --git a/ebean-api/src/test/java/io/ebean/TestWeakRefTempFileProvider.java b/ebean-api/src/test/java/io/ebean/TestWeakRefTempFileProvider.java
new file mode 100644
index 0000000000..dc69cc6876
--- /dev/null
+++ b/ebean-api/src/test/java/io/ebean/TestWeakRefTempFileProvider.java
@@ -0,0 +1,171 @@
+package io.ebean;
+
+
+import io.ebean.config.WeakRefTempFileProvider;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.nio.channels.FileLock;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Test for the WeakRefTempFileProvider. (Note: this test relies on an aggressive garbage collection.
+ * if GC implementation will change, the test may fail)
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public class TestWeakRefTempFileProvider {
+
+ WeakRefTempFileProvider prov = new WeakRefTempFileProvider();
+
+ @AfterEach
+ public void shutdown() {
+ prov.shutdown();
+ }
+
+ /**
+ * Run the garbage collection and delete stale files.
+ */
+ private void gc() throws InterruptedException {
+ System.gc();
+ Thread.sleep(100);
+ prov.deleteStaleTempFiles();
+ }
+
+ @Test
+ public void testStaleEntries() throws Exception {
+ File tempFile = prov.createTempFile();
+ String fileName = tempFile.getAbsolutePath();
+
+ gc();
+
+ assertThat(new File(fileName)).exists();
+
+ tempFile = null; // give up reference
+
+ gc();
+
+ assertThat(new File(fileName)).doesNotExist();
+
+
+ }
+
+ @Test
+ public void testLinkedListForward() throws Exception {
+ File tempFile1 = prov.createTempFile();
+ String fileName1 = tempFile1.getAbsolutePath();
+ File tempFile2 = prov.createTempFile();
+ String fileName2 = tempFile2.getAbsolutePath();
+ File tempFile3 = prov.createTempFile();
+ String fileName3 = tempFile3.getAbsolutePath();
+
+ assertThat(new File(fileName1)).exists();
+ assertThat(new File(fileName2)).exists();
+ assertThat(new File(fileName3)).exists();
+
+ gc();
+
+ // give up first ref
+ tempFile1 = null;
+
+ gc();
+
+ assertThat(new File(fileName1)).doesNotExist();
+ assertThat(new File(fileName2)).exists();
+ assertThat(new File(fileName3)).exists();
+
+ // give up second ref
+ tempFile2 = null;
+
+ gc();
+
+ assertThat(new File(fileName1)).doesNotExist();
+ assertThat(new File(fileName2)).doesNotExist();
+ assertThat(new File(fileName3)).exists();
+
+ // give up third ref
+ tempFile3 = null;
+
+ gc();
+
+ assertThat(new File(fileName1)).doesNotExist();
+ assertThat(new File(fileName2)).doesNotExist();
+ assertThat(new File(fileName3)).doesNotExist();
+
+ }
+
+
+ @Test
+ public void testLinkedListReverse() throws Exception {
+ File tempFile1 = prov.createTempFile();
+ String fileName1 = tempFile1.getAbsolutePath();
+ File tempFile2 = prov.createTempFile();
+ String fileName2 = tempFile2.getAbsolutePath();
+ File tempFile3 = prov.createTempFile();
+ String fileName3 = tempFile3.getAbsolutePath();
+
+ assertThat(new File(fileName1)).exists();
+ assertThat(new File(fileName2)).exists();
+ assertThat(new File(fileName3)).exists();
+
+ gc();
+
+ // give up third ref
+ tempFile3 = null;
+
+ gc();
+
+ assertThat(new File(fileName1)).exists();
+ assertThat(new File(fileName2)).exists();
+ assertThat(new File(fileName3)).doesNotExist();
+
+ // give up second ref
+ tempFile2 = null;
+
+ gc();
+
+ assertThat(new File(fileName1)).exists();
+ assertThat(new File(fileName2)).doesNotExist();
+ assertThat(new File(fileName3)).doesNotExist();
+
+ // give up first ref
+ tempFile1 = null;
+
+ gc();
+
+ assertThat(new File(fileName1)).doesNotExist();
+ assertThat(new File(fileName2)).doesNotExist();
+ assertThat(new File(fileName3)).doesNotExist();
+
+ }
+
+ @Test
+ @Disabled("Runs on Windows only")
+ public void testFileLocked() throws Exception {
+ File tempFile = prov.createTempFile();
+ String fileName = tempFile.getAbsolutePath();
+
+ try (FileOutputStream os = new FileOutputStream(fileName)) {
+ FileLock lock = os.getChannel().lock();
+ try {
+ os.write(42);
+
+ tempFile = null;
+ gc();
+ } finally {
+ lock.release();
+ }
+
+ }
+
+ assertThat(new File(fileName)).exists();
+
+ prov.shutdown();
+
+ assertThat(new File(fileName)).doesNotExist();
+ }
+}
diff --git a/ebean-autotune/pom.xml b/ebean-autotune/pom.xml
index d425dda80a..673ad765f2 100644
--- a/ebean-autotune/pom.xml
+++ b/ebean-autotune/pom.xml
@@ -4,7 +4,7 @@
ebean-parent
io.ebean
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
@@ -13,8 +13,8 @@
- scm:git:git@github.com:ebean-orm/ebean.git
- HEAD
+ scm:git:git@github.com:FOCONIS/ebean.git
+ ebean-parent-13.6.4-FOC1
ebean autotune
@@ -26,7 +26,7 @@
io.ebean
ebean-core
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
provided
@@ -62,7 +62,7 @@
io.ebean
ebean-ddl-generator
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
test
@@ -76,7 +76,7 @@
io.ebean
ebean-platform-h2
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
test
diff --git a/ebean-bom/pom.xml b/ebean-bom/pom.xml
index dbcf5300b7..16118d56ba 100644
--- a/ebean-bom/pom.xml
+++ b/ebean-bom/pom.xml
@@ -4,7 +4,7 @@
ebean-parent
io.ebean
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
ebean bom
@@ -89,112 +89,112 @@
io.ebean
ebean
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-api
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-core
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-core-type
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-joda-time
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-jackson-jsonnode
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-jackson-mapper
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-ddl-generator
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-externalmapping-api
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-externalmapping-xml
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-autotune
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-querybean
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
querybean-generator
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
provided
io.ebean
kotlin-querybean-generator
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
provided
io.ebean
ebean-test
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
test
io.ebean
ebean-postgis
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-redis
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-spring-txn
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
@@ -202,67 +202,67 @@
io.ebean
ebean-clickhouse
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-db2
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-h2
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-hana
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-mariadb
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-mysql
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-nuodb
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-oracle
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-postgres
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-sqlite
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-sqlserver
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
diff --git a/ebean-core-type/pom.xml b/ebean-core-type/pom.xml
index 3dff6a7cfb..f4b89aa05f 100644
--- a/ebean-core-type/pom.xml
+++ b/ebean-core-type/pom.xml
@@ -4,7 +4,7 @@
ebean-parent
io.ebean
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
ebean-core-type
@@ -16,7 +16,7 @@
io.ebean
ebean-api
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
diff --git a/ebean-core/pom.xml b/ebean-core/pom.xml
index c6fc054fb0..63d035fd4e 100644
--- a/ebean-core/pom.xml
+++ b/ebean-core/pom.xml
@@ -3,7 +3,7 @@
ebean-parent
io.ebean
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
ebean-core
@@ -14,15 +14,15 @@
https://ebean.io/
- scm:git:git@github.com:ebean-orm/ebean.git
- HEAD
+ scm:git:git@github.com:FOCONIS/ebean.git
+ ebean-parent-13.6.4-FOC1
io.ebean
ebean-api
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
@@ -46,13 +46,13 @@
io.ebean
ebean-core-type
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
io.ebean
ebean-externalmapping-api
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
@@ -143,21 +143,21 @@
io.ebean
ebean-platform-h2
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
test
io.ebean
ebean-platform-postgres
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
test
io.ebean
ebean-platform-sqlserver
- 13.17.3
+ 13.17.3-FOC12-SNAPSHOT
test
@@ -229,7 +229,6 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 3.1.1
Ebean 12
src/main/java/io/ebean/overview.html
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java
index a207f3c573..9f77331016 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java
@@ -7,13 +7,15 @@ public final class HashQuery {
private final CQueryPlanKey planHash;
private final BindValuesKey bindValuesKey;
+ private final Class> dtoType;
/**
* Create the HashQuery.
*/
- public HashQuery(CQueryPlanKey planHash, BindValuesKey bindValuesKey) {
+ public HashQuery(CQueryPlanKey planHash, BindValuesKey bindValuesKey, Class> dtoType) {
this.planHash = planHash;
this.bindValuesKey = bindValuesKey;
+ this.dtoType = dtoType;
}
@Override
@@ -25,6 +27,7 @@ public String toString() {
public int hashCode() {
int hc = 92821 * planHash.hashCode();
hc = 92821 * hc + bindValuesKey.hashCode();
+ hc = 92821 * hc + dtoType.hashCode();
return hc;
}
@@ -37,6 +40,6 @@ public boolean equals(Object obj) {
return false;
}
HashQuery e = (HashQuery) obj;
- return e.bindValuesKey.equals(bindValuesKey) && e.planHash.equals(planHash);
+ return e.bindValuesKey.equals(bindValuesKey) && e.planHash.equals(planHash) && e.dtoType.equals(dtoType);
}
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java
index 135976f540..c8a4210055 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java
@@ -2,14 +2,16 @@
import io.ebean.meta.MetaQueryPlan;
+import java.time.Instant;
+
/**
* Internal database query plan being capture.
*/
public interface SpiDbQueryPlan extends MetaQueryPlan {
/**
- * Extend with queryTimeMicros and captureCount.
+ * Extend with queryTimeMicros, captureCount, captureMicros and when the bind values were captured.
*/
- SpiDbQueryPlan with(long queryTimeMicros, long captureCount);
+ SpiDbQueryPlan with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured, Object tenantId);
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java
index b3c4858139..d821e75b84 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java
@@ -12,4 +12,9 @@ public interface SpiDdlGenerator {
*/
void execute(boolean online);
+ /**
+ * Run DDL manually. This can be used to initialize multi tenant environments or if you plan not to run
+ * DDL on startup
+ */
+ void runDdl();
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java
index bcef08e57e..344b428c9e 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java
@@ -1,5 +1,6 @@
package io.ebeaninternal.api;
+import io.avaje.lang.Nullable;
import io.ebean.*;
import io.ebean.bean.BeanCollectionLoader;
import io.ebean.bean.CallOrigin;
@@ -14,7 +15,6 @@
import io.ebeaninternal.server.query.CQuery;
import io.ebeaninternal.server.transaction.RemoteTransactionEvent;
-import javax.annotation.Nullable;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -197,6 +197,11 @@ public interface SpiEbeanServer extends SpiServer, ExtendedServer, BeanCollectio
*/
DataTimeZone dataTimeZone();
+ /**
+ * Returns the maximum string size in bind values.
+ */
+ int maxStringSize();
+
/**
* Check for slow query event.
*/
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java
index e10b863152..f4e9d03fca 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java
@@ -842,6 +842,11 @@ public static TemporalMode of(SpiQuery> query) {
*/
void setManualId();
+ /**
+ * Set the DTO type, that should be part of the queryHash.
+ */
+ void setDtoType(Class> dtoType);
+
/**
* Set default select clauses where none have been explicitly defined.
*/
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java
index a8acdb45a7..320c1d6faf 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java
@@ -104,6 +104,11 @@ public interface SpiTransaction extends Transaction {
*/
Boolean isUpdateAllLoadedProperties();
+ /**
+ * Returns true, if generated properties are overwritten (default) or are only set, if they are null.
+ */
+ boolean isOverwriteGeneratedProperties();
+
/**
* Return the batchSize specifically set for this transaction or 0.
*
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java
index 01311d1673..cc0ff69efa 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java
@@ -60,6 +60,6 @@ public interface SpiTransactionManager {
/**
* Return a connection used for query plan collection.
*/
- Connection queryPlanConnection() throws SQLException;
+ Connection queryPlanConnection(Object tenantId) throws SQLException;
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java
index 87c2f606ab..88fc870aa8 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java
@@ -248,6 +248,16 @@ public void setUpdateAllLoadedProperties(boolean updateAllLoaded) {
transaction.setUpdateAllLoadedProperties(updateAllLoaded);
}
+ @Override
+ public void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties) {
+ transaction.setOverwriteGeneratedProperties(overwriteGeneratedProperties);
+ }
+
+ @Override
+ public boolean isOverwriteGeneratedProperties() {
+ return transaction.isOverwriteGeneratedProperties();
+ }
+
@Override
public Boolean isUpdateAllLoadedProperties() {
return transaction.isUpdateAllLoadedProperties();
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java b/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java
index 613b105cc6..4ed612defc 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java
@@ -34,5 +34,4 @@ public interface SpiJsonReader {
Object readValueUsingObjectMapper(Class> propertyType) throws IOException;
- boolean update();
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBind.java b/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBind.java
index 3c4c1aedb8..89c5eebccb 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBind.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBind.java
@@ -4,7 +4,11 @@
import io.ebeaninternal.api.CoreLog;
import io.ebeaninternal.server.core.timezone.DataTimeZone;
-import java.io.*;
+import javax.persistence.PersistenceException;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
import java.math.BigDecimal;
import java.sql.*;
import java.util.ArrayList;
@@ -16,15 +20,19 @@
public class DataBind implements DataBinder {
private final DataTimeZone dataTimeZone;
+ private final int maxStringSize;
private final PreparedStatement pstmt;
private final Connection connection;
private final StringBuilder bindLog = new StringBuilder();
+
private List inputStreams;
protected int pos;
private String json;
- public DataBind(DataTimeZone dataTimeZone, PreparedStatement pstmt, Connection connection) {
+
+ public DataBind(DataTimeZone dataTimeZone, int maxStringSize, PreparedStatement pstmt, Connection connection) {
this.dataTimeZone = dataTimeZone;
+ this.maxStringSize = maxStringSize;
this.pstmt = pstmt;
this.connection = connection;
}
@@ -116,6 +124,10 @@ public final PreparedStatement getPstmt() {
@Override
public void setString(String value) throws SQLException {
+ if (maxStringSize > 0 && maxStringSize < value.length()) {
+ throw new PersistenceException("The value '" + value.substring(0, 50) + "...' ("
+ + value.length() + " chars) exceeds the max string size of " + maxStringSize + " chars)");
+ }
pstmt.setString(++pos, value);
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBindCapture.java b/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBindCapture.java
index 78924bafef..c2bbe0e21a 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBindCapture.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/bind/DataBindCapture.java
@@ -1,8 +1,8 @@
package io.ebeaninternal.server.bind;
-import io.ebeaninternal.server.core.timezone.DataTimeZone;
import io.ebeaninternal.server.bind.capture.BindCapture;
import io.ebeaninternal.server.bind.capture.BindCaptureStatement;
+import io.ebeaninternal.server.core.timezone.DataTimeZone;
/**
* Special DataBind used to capture bind values for obtaining explain plans.
@@ -14,12 +14,12 @@ public final class DataBindCapture extends DataBind {
/**
* Create given the dataTimeZone in use.
*/
- public static DataBindCapture of(DataTimeZone dataTimeZone) {
- return new DataBindCapture(dataTimeZone, new BindCaptureStatement());
+ public static DataBindCapture of(DataTimeZone dataTimeZone, int maxStringSize) {
+ return new DataBindCapture(dataTimeZone, new BindCaptureStatement(), maxStringSize);
}
- private DataBindCapture(DataTimeZone dataTimeZone, BindCaptureStatement pstmt) {
- super(dataTimeZone, pstmt, null);
+ private DataBindCapture(DataTimeZone dataTimeZone, BindCaptureStatement pstmt, int maxStringSize) {
+ super(dataTimeZone, maxStringSize, pstmt, null);
this.captureStatement = pstmt;
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java
index ae0c10f4c1..d1ed0efdfd 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java
@@ -13,18 +13,20 @@
public final class CacheManagerOptions {
private final ClusterManager clusterManager;
- private final DatabaseConfig databaseConfig;
+ private final String serverName;
private final boolean localL2Caching;
private CurrentTenantProvider currentTenantProvider;
private QueryCacheEntryValidate queryCacheEntryValidate;
private ServerCacheFactory cacheFactory = new DefaultServerCacheFactory();
private ServerCacheOptions beanDefault = new ServerCacheOptions();
private ServerCacheOptions queryDefault = new ServerCacheOptions();
+ private final boolean tenantPartitionedCache;
CacheManagerOptions() {
this.localL2Caching = true;
this.clusterManager = null;
- this.databaseConfig = null;
+ this.serverName = "db";
+ this.tenantPartitionedCache = false;
this.cacheFactory = new DefaultServerCacheFactory();
this.beanDefault = new ServerCacheOptions();
this.queryDefault = new ServerCacheOptions();
@@ -32,9 +34,10 @@ public final class CacheManagerOptions {
public CacheManagerOptions(ClusterManager clusterManager, DatabaseConfig config, boolean localL2Caching) {
this.clusterManager = clusterManager;
- this.databaseConfig = config;
+ this.serverName = config.getName();
this.localL2Caching = localL2Caching;
this.currentTenantProvider = config.getCurrentTenantProvider();
+ this.tenantPartitionedCache = config.isTenantPartitionedCache();
}
public CacheManagerOptions with(ServerCacheOptions beanDefault, ServerCacheOptions queryDefault) {
@@ -55,7 +58,7 @@ public CacheManagerOptions with(CurrentTenantProvider currentTenantProvider) {
}
public String getServerName() {
- return (databaseConfig == null) ? "db" : databaseConfig.getName();
+ return serverName;
}
public boolean isLocalL2Caching() {
@@ -85,4 +88,6 @@ public ClusterManager getClusterManager() {
public QueryCacheEntryValidate getQueryCacheEntryValidate() {
return queryCacheEntryValidate;
}
+
+ public boolean isTenantPartitionedCache() { return tenantPartitionedCache; }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java
index aba5ab3638..06d322ecd1 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java
@@ -32,6 +32,7 @@ final class DefaultCacheHolder {
private final ServerCacheOptions queryDefault;
private final CurrentTenantProvider tenantProvider;
private final QueryCacheEntryValidate queryCacheEntryValidate;
+ private final boolean tenantPartitionedCache;
DefaultCacheHolder(CacheManagerOptions builder) {
this.cacheFactory = builder.getCacheFactory();
@@ -39,6 +40,7 @@ final class DefaultCacheHolder {
this.queryDefault = builder.getQueryDefault();
this.tenantProvider = builder.getCurrentTenantProvider();
this.queryCacheEntryValidate = builder.getQueryCacheEntryValidate();
+ this.tenantPartitionedCache = builder.isTenantPartitionedCache();
}
void visitMetrics(MetricVisitor visitor) {
@@ -56,16 +58,42 @@ ServerCache getCache(Class> beanType, String collectionProperty) {
return getCacheInternal(beanType, ServerCacheType.COLLECTION_IDS, collectionProperty);
}
+ private String key(String beanName) {
+ if (tenantPartitionedCache) {
+ StringBuilder sb = new StringBuilder(beanName.length() + 64);
+ sb.append(beanName);
+ sb.append('.');
+ sb.append(tenantProvider.currentId());
+ return sb.toString();
+ } else {
+ return beanName;
+ }
+ }
+
private String key(String beanName, ServerCacheType type) {
- return beanName + type.code();
+ StringBuilder sb = new StringBuilder(beanName.length() + 64);
+ sb.append(beanName);
+ if (tenantPartitionedCache) {
+ sb.append('.');
+ sb.append(tenantProvider.currentId());
+ }
+ sb.append(type.code());
+ return sb.toString();
}
private String key(String beanName, String collectionProperty, ServerCacheType type) {
+ StringBuilder sb = new StringBuilder(beanName.length() + 64);
+ sb.append(beanName);
+ if (tenantPartitionedCache) {
+ sb.append('.');
+ sb.append(tenantProvider.currentId());
+ }
if (collectionProperty != null) {
- return beanName + "." + collectionProperty + type.code();
- } else {
- return beanName + type.code();
+ sb.append('.');
+ sb.append(collectionProperty);
}
+ sb.append(type.code());
+ return sb.toString();
}
/**
@@ -82,12 +110,17 @@ private ServerCache createCache(Class> beanType, ServerCacheType type, String
if (type == ServerCacheType.COLLECTION_IDS) {
lock.lock();
try {
- collectIdCaches.computeIfAbsent(beanType.getName(), s -> new ConcurrentSkipListSet<>()).add(key);
+ collectIdCaches.computeIfAbsent(key(beanType.getName()), s -> new ConcurrentSkipListSet<>()).add(key);
} finally {
lock.unlock();
}
}
- return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, tenantProvider, queryCacheEntryValidate));
+ if (tenantPartitionedCache) {
+ return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, null, queryCacheEntryValidate));
+ } else {
+ return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, tenantProvider, queryCacheEntryValidate));
+ }
+
}
void clearAll() {
@@ -103,7 +136,7 @@ public void clear(String name) {
clearIfExists(key(name, ServerCacheType.QUERY));
clearIfExists(key(name, ServerCacheType.BEAN));
clearIfExists(key(name, ServerCacheType.NATURAL_KEY));
- Set keys = collectIdCaches.get(name);
+ Set keys = collectIdCaches.get(key(name));
if (keys != null) {
for (String collectionIdKey : keys) {
clearIfExists(collectionIdKey);
@@ -147,4 +180,7 @@ private ServerCacheOptions getBeanOptions(Class> cls) {
return beanDefault.copy(nearCache);
}
+ boolean isTenantPartitionedCache() {
+ return tenantPartitionedCache;
+ }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java
index 2590265716..a0eea215b2 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java
@@ -6,6 +6,7 @@
import io.ebean.cache.ServerCacheStatistics;
import io.ebean.meta.MetricVisitor;
import io.ebean.metric.CountMetric;
+import io.ebean.metric.Metric;
import io.ebean.metric.MetricFactory;
import java.io.Serializable;
@@ -48,6 +49,7 @@ public class DefaultServerCache implements ServerCache {
protected final CountMetric idleCount;
protected final CountMetric ttlCount;
protected final CountMetric lruCount;
+ protected final Metric sizeCount;
protected final String name;
protected final String shortName;
protected final int maxSize;
@@ -81,6 +83,7 @@ public DefaultServerCache(DefaultServerCacheConfig config) {
this.idleCount = factory.createCountMetric(prefix + shortName + ".idle");
this.ttlCount = factory.createCountMetric(prefix + shortName + ".ttl");
this.lruCount = factory.createCountMetric(prefix + shortName + ".lru");
+ this.sizeCount = factory.createMetric(prefix + shortName + ".size", map::size);
}
@@ -103,6 +106,7 @@ public void visit(MetricVisitor visitor) {
idleCount.visit(visitor);
ttlCount.visit(visitor);
lruCount.visit(visitor);
+ sizeCount.visit(visitor);
}
@Override
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java
index 0b7264ff90..39558d19d8 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java
@@ -154,4 +154,8 @@ public ServerCache getBeanCache(Class> beanType) {
return cacheHolder.getCache(beanType, ServerCacheType.BEAN);
}
+ @Override
+ public boolean isTenantPartitionedCache() {
+ return cacheHolder.isTenantPartitionedCache();
+ }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java
index bf6d4d5a21..ebeec00f68 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java
@@ -88,4 +88,10 @@ public interface SpiCacheManager {
*/
void clearLocal(Class> beanType);
+ /**
+ * returns true, if this chacheManager runs in tenant partitioned mode
+ * @return
+ */
+ boolean isTenantPartitionedCache();
+
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java
index b4a0c5b202..245032121c 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java
@@ -126,7 +126,7 @@ private String limitOffset(String sql) {
/**
* Prepare and execute the SQL using the Binder.
*/
- public void executeSql(Binder binder, SpiQuery.Type type) throws SQLException {
+ public void executeSql(Binder binder) throws SQLException {
startNano = System.nanoTime();
executeAsSql(binder);
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/BeanRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/BeanRequest.java
index b8224bc23b..d0a0afc495 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/BeanRequest.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/BeanRequest.java
@@ -123,4 +123,8 @@ public boolean logSummary() {
public DataTimeZone dataTimeZone() {
return server.dataTimeZone();
}
+
+ public int maxStringSize() {
+ return server.maxStringSize();
+ }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java
index c579807f9b..3fab82bd09 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java
@@ -137,7 +137,7 @@ private void applyConfigServices(DatabaseConfig config) {
private void checkMissingModulePathProvides() {
URL servicesFile = ClassLoader.getSystemResource("META-INF/services/io.ebean.config.EntityClassRegister");
if (servicesFile != null) {
- log.log(ERROR, "module-info.java is probably missing 'provides io.ebean.config.EntityClassRegister with EbeanEntityRegister' clause. EntityClassRegister exists but was not service loaded.");
+ log.log(ERROR, "module-info.java is probably missing ''provides io.ebean.config.EntityClassRegister with EbeanEntityRegister'' clause. EntityClassRegister exists but was not service loaded.");
}
}
@@ -169,6 +169,7 @@ private BootupClasses bootupClasses(DatabaseConfig config) {
bootup.addFindControllers(config.getFindControllers());
bootup.addPersistListeners(config.getPersistListeners());
bootup.addQueryAdapters(config.getQueryAdapters());
+ bootup.addCustomDeployParser(config.getCustomDeployParsers());
bootup.addChangeLogInstances(config);
return bootup;
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultMetaInfoManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultMetaInfoManager.java
index b4a29697c3..a4b2aaaa77 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultMetaInfoManager.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultMetaInfoManager.java
@@ -45,6 +45,11 @@ public BasicMetricVisitor visitBasic() {
return basic;
}
+ @Override
+ public MetricReportGenerator createReportGenerator() {
+ return new HtmlMetricReportGenerator(server);
+ }
+
@Override
public void resetAllMetrics() {
server.visitMetrics(new ResetVisitor());
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultQueryPlanListener.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultQueryPlanListener.java
index 0393240725..f38b9c8fab 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultQueryPlanListener.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultQueryPlanListener.java
@@ -18,9 +18,9 @@ public void process(QueryPlanCapture capture) {
// better to log this in JSON form?
String dbName = capture.database().name();
for (MetaQueryPlan plan : capture.plans()) {
- log.log(INFO, "queryPlan db:{0} label:{1} queryTimeMicros:{2} loc:{3} sql:{4} bind:{5} plan:{6}",
- dbName, plan.label(), plan.queryTimeMicros(), plan.profileLocation(),
- plan.sql(), plan.bind(), plan.plan());
+ log.log(INFO, "queryPlan db:{0} label:{1} queryTimeMicros:{2} captureMicros:{3} whenCaptured:{4} captureCount:{5} loc:{6} sql:{7} bind:{8} plan:{9}",
+ dbName, plan.label(), plan.queryTimeMicros(), plan.captureMicros(), plan.whenCaptured(), plan.captureCount(),
+ plan.profileLocation(), plan.sql(), plan.bind(), plan.plan());
}
}
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
index 98356f4186..b56b8e559c 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
@@ -16,6 +16,8 @@
import io.ebean.event.readaudit.ReadAuditLogger;
import io.ebean.event.readaudit.ReadAuditPrepare;
import io.ebean.meta.*;
+import io.ebean.metric.Metric;
+import io.ebean.metric.MetricRegistry;
import io.ebean.migration.auto.AutoMigrationRunner;
import io.ebean.plugin.BeanType;
import io.ebean.plugin.Plugin;
@@ -79,9 +81,11 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
private final String serverName;
private final DatabasePlatform databasePlatform;
private final TransactionManager transactionManager;
+ private final TempFileProvider tempFileProvider;
private final QueryPlanManager queryPlanManager;
private final ExtraMetrics extraMetrics;
private final DataTimeZone dataTimeZone;
+ private final int maxStringSize;
private final ClockService clockService;
private final CallOriginFactory callStackFactory;
private final Persister persister;
@@ -150,6 +154,7 @@ public DefaultServer(InternalConfiguration config, ServerCacheManager cache) {
this.beanLoader = new DefaultBeanLoader(this);
this.jsonContext = config.createJsonContext(this);
this.dataTimeZone = config.getDataTimeZone();
+ this.maxStringSize = config.getMaxStringSize();
this.clockService = config.getClockService();
DocStoreIntegration docStoreComponents = config.createDocStoreIntegration(this);
@@ -158,6 +163,7 @@ public DefaultServer(InternalConfiguration config, ServerCacheManager cache) {
this.queryPlanManager = config.initQueryPlanManager(transactionManager);
this.metaInfoManager = new DefaultMetaInfoManager(this, this.config.getMetricNaming());
this.serverPlugins = config.getPlugins();
+ this.tempFileProvider = config.getConfig().getTempFileProvider();
this.ddlGenerator = config.initDdlGenerator(this);
this.scriptRunner = new DScriptRunner(this);
@@ -242,6 +248,11 @@ public DataTimeZone dataTimeZone() {
return dataTimeZone;
}
+ @Override
+ public int maxStringSize() {
+ return maxStringSize;
+ }
+
@Override
public MetaInfoManager metaInfo() {
return metaInfoManager;
@@ -373,6 +384,8 @@ public void shutdown(boolean shutdownDataSource, boolean deregisterDriver) {
backgroundExecutor.shutdown();
// shutdown DataSource (if its an Ebean one)
transactionManager.shutdown(shutdownDataSource, deregisterDriver);
+ tempFileProvider.shutdown();
+
dumpMetrics();
shutdown = true;
if (shutdownDataSource) {
@@ -586,7 +599,11 @@ public void clearQueryStatistics() {
*/
@Override
public T createEntityBean(Class type) {
- return descriptor(type).createBean();
+ final BeanDescriptor desc = descriptor(type);
+ if (desc == null) {
+ throw new IllegalArgumentException("No bean type " + type.getName() + " registered");
+ }
+ return desc.createBean();
}
/**
@@ -815,6 +832,12 @@ public void merge(Object bean, MergeOptions options, @Nullable Transaction trans
executeInTrans((txn) -> persister.merge(desc, checkEntityBean(bean), options, txn), transaction);
}
+ @Override
+ public T mergeBeans(T bean, T existing, BeanMergeOptions options) {
+ BeanDescriptor> desc = desc(bean.getClass());
+ return (T) desc.mergeBeans(checkEntityBean(bean), (EntityBean) existing, options);
+ }
+
@Override
public void lock(Object bean) {
BeanDescriptor> desc = desc(bean.getClass());
@@ -890,6 +913,7 @@ public DtoQuery createNamedDtoQuery(Class dtoType, String namedQuery)
@Override
public DtoQuery findDto(Class dtoType, SpiQuery> ormQuery) {
DtoBeanDescriptor descriptor = dtoBeanManager.getDescriptor(dtoType);
+ ormQuery.setDtoType(dtoType);
return new DefaultDtoQuery<>(this, descriptor, ormQuery);
}
@@ -930,6 +954,18 @@ public T find(Class beanType, Object id, @Nullable Transaction transactio
return findId(query, transaction);
}
+ DtoQueryRequest createDtoQueryRequest(Type type, SpiDtoQuery query) {
+ SpiQuery> ormQuery = query.getOrmQuery();
+ if (ormQuery != null) {
+ ormQuery.setType(type);
+ ormQuery.setManualId();
+ SpiOrmQueryRequest> ormRequest = createQueryRequest(type, ormQuery, query.getTransaction());
+ return new DtoQueryRequest<>(this, dtoQueryEngine, query, ormRequest);
+ } else {
+ return new DtoQueryRequest<>(this, dtoQueryEngine, query, null);
+ }
+ }
+
SpiOrmQueryRequest createQueryRequest(Type type, Query query, @Nullable Transaction transaction) {
SpiOrmQueryRequest request = buildQueryRequest(type, query, transaction);
request.prepareQuery();
@@ -967,40 +1003,6 @@ private SpiOrmQueryRequest buildQueryRequest(SpiQuery query, @Nullable
return new OrmQueryRequest<>(this, queryEngine, query, (SpiTransaction) transaction);
}
- /**
- * Try to get the object out of the persistence context.
- */
- @Nullable
- @SuppressWarnings("unchecked")
- private T findIdCheckPersistenceContextAndCache(@Nullable Transaction transaction, SpiQuery query, Object id) {
- SpiTransaction t = (SpiTransaction) transaction;
- if (t == null) {
- t = currentServerTransaction();
- }
- BeanDescriptor desc = query.getBeanDescriptor();
- id = desc.convertId(id);
- PersistenceContext pc = null;
- if (t != null && useTransactionPersistenceContext(query)) {
- // first look in the transaction scoped persistence context
- pc = t.getPersistenceContext();
- if (pc != null) {
- WithOption o = desc.contextGetWithOption(pc, id);
- if (o != null) {
- if (o.isDeleted()) {
- // Bean was previously deleted in the same transaction / persistence context
- return null;
- }
- return (T) o.getBean();
- }
- }
- }
- if (!query.isBeanCacheGet() || (t != null && t.isSkipCache())) {
- return null;
- }
- // Hit the L2 bean cache
- return desc.cacheBeanGet(id, query.isReadOnly(), pc);
- }
-
/**
* Return true if transactions PersistenceContext should be used.
*/
@@ -1022,15 +1024,59 @@ public PersistenceContextScope persistenceContextScope(SpiQuery> query) {
private T findId(Query query, @Nullable Transaction transaction) {
SpiQuery spiQuery = (SpiQuery) query;
spiQuery.setType(Type.BEAN);
+ SpiOrmQueryRequest request = null;
if (SpiQuery.Mode.NORMAL == spiQuery.getMode() && !spiQuery.isForceHitDatabase()) {
// See if we can skip doing the fetch completely by getting the bean from the
// persistence context or the bean cache
- T bean = findIdCheckPersistenceContextAndCache(transaction, spiQuery, spiQuery.getId());
- if (bean != null) {
- return bean;
+ SpiTransaction t = (SpiTransaction) transaction;
+ if (t == null) {
+ t = currentServerTransaction();
+ }
+ BeanDescriptor desc = spiQuery.getBeanDescriptor();
+ Object id = desc.convertId(spiQuery.getId());
+ PersistenceContext pc = null;
+ if (t != null && useTransactionPersistenceContext(spiQuery)) {
+ // first look in the transaction scoped persistence context
+ pc = t.getPersistenceContext();
+ if (pc != null) {
+ WithOption o = desc.contextGetWithOption(pc, id);
+ if (o != null) {
+ // We have found a hit. This could be also one with o.deleted() == true
+ // if bean was previously deleted in the same transaction / persistence context
+ return (T) o.getBean();
+ }
+ }
+ }
+ if (t == null || !t.isSkipCache()) {
+ if (spiQuery.getUseQueryCache() != CacheMode.OFF) {
+ request = buildQueryRequest(spiQuery, transaction);
+ if (request.isQueryCacheActive()) {
+ // Hit the query cache
+ request.prepareQuery();
+ T bean = request.getFromQueryCache();
+ if (bean != null) {
+ return bean;
+ }
+ }
+ }
+ if (spiQuery.isBeanCacheGet()) {
+ // Hit the L2 bean cache
+ T bean = desc.cacheBeanGet(id, spiQuery.isReadOnly(), pc);
+ if (bean != null) {
+ if (request != null && request.isQueryCachePut()) {
+ // copy bean from the L2 cache to the faster query cache, if caching is enabled
+ request.prepareQuery();
+ request.putToQueryCache(bean);
+ }
+ return bean;
+ }
+ }
}
}
- SpiOrmQueryRequest request = buildQueryRequest(spiQuery, transaction);
+
+ if (request == null) {
+ request = buildQueryRequest(spiQuery, transaction);
+ }
request.prepareQuery();
if (request.isUseDocStore()) {
return docStore().find(request);
@@ -1080,15 +1126,18 @@ private T extractUnique(List list) {
public Set findSet(Query query, Transaction transaction) {
SpiOrmQueryRequest request = buildQueryRequest(Type.SET, query, transaction);
request.resetBeanCacheAutoMode(false);
+ if (request.isQueryCacheActive()) {
+ request.prepareQuery();
+ Object result = request.getFromQueryCache();
+ if (result != null) {
+ return (Set) result;
+ }
+ }
if ((transaction == null || !transaction.isSkipCache()) && request.getFromBeanCache()) {
// hit bean cache and got all results from cache
return request.beanCacheHitsAsSet();
}
request.prepareQuery();
- Object result = request.getFromQueryCache();
- if (result != null) {
- return (Set) result;
- }
try {
request.initTransIfRequired();
return request.findSet();
@@ -1101,16 +1150,19 @@ public Set findSet(Query query, Transaction transaction) {
@SuppressWarnings({"unchecked", "rawtypes"})
public Map findMap(Query query, @Nullable Transaction transaction) {
SpiOrmQueryRequest request = buildQueryRequest(Type.MAP, query, transaction);
+ if (request.isQueryCacheActive()) {
+ request.prepareQuery();
+ Object result = request.getFromQueryCache();
+ if (result != null) {
+ return (Map) result;
+ }
+ }
request.resetBeanCacheAutoMode(false);
if ((transaction == null || !transaction.isSkipCache()) && request.getFromBeanCache()) {
// hit bean cache and got all results from cache
return request.beanCacheHitsAsMap();
}
request.prepareQuery();
- Object result = request.getFromQueryCache();
- if (result != null) {
- return (Map) result;
- }
try {
request.initTransIfRequired();
return request.findMap();
@@ -1387,15 +1439,18 @@ public List findList(Query query, Transaction transaction) {
private List findList(Query query, @Nullable Transaction transaction, boolean findOne) {
SpiOrmQueryRequest request = buildQueryRequest(Type.LIST, query, transaction);
request.resetBeanCacheAutoMode(findOne);
+ if (request.isQueryCacheActive()) {
+ request.prepareQuery();
+ Object result = request.getFromQueryCache();
+ if (result != null) {
+ return (List) result;
+ }
+ }
if ((transaction == null || !transaction.isSkipCache()) && request.getFromBeanCache()) {
// hit bean cache and got all results from cache
return request.beanCacheHits();
}
request.prepareQuery();
- Object result = request.getFromQueryCache();
- if (result != null) {
- return (List) result;
- }
if (request.isUseDocStore()) {
return docStore().findList(request);
}
@@ -1491,7 +1546,7 @@ public T findSingleAttribute(SpiSqlQuery query, Class cls) {
@Override
public void findDtoEach(SpiDtoQuery query, Consumer consumer) {
- DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query);
+ DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query);
try {
request.initTransIfRequired();
request.findEach(consumer);
@@ -1502,7 +1557,7 @@ public void findDtoEach(SpiDtoQuery query, Consumer consumer) {
@Override
public void findDtoEach(SpiDtoQuery query, int batch, Consumer> consumer) {
- DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query);
+ DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query);
try {
request.initTransIfRequired();
request.findEach(batch, consumer);
@@ -1513,7 +1568,7 @@ public void findDtoEach(SpiDtoQuery query, int batch, Consumer> c
@Override
public void findDtoEachWhile(SpiDtoQuery query, Predicate consumer) {
- DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query);
+ DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query);
try {
request.initTransIfRequired();
request.findEachWhile(consumer);
@@ -1524,7 +1579,7 @@ public void findDtoEachWhile(SpiDtoQuery query, Predicate consumer) {
@Override
public QueryIterator findDtoIterate(SpiDtoQuery query) {
- DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query);
+ DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query);
try {
request.initTransIfRequired();
return request.findIterate();
@@ -1541,7 +1596,11 @@ public Stream findDtoStream(SpiDtoQuery query) {
@Override
public List findDtoList(SpiDtoQuery query) {
- DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query);
+ DtoQueryRequest request = createDtoQueryRequest(Type.LIST, query);
+ List ret = request.getFromQueryCache();
+ if (ret != null) {
+ return ret;
+ }
try {
request.initTransIfRequired();
return request.findList();
@@ -1553,13 +1612,7 @@ public List findDtoList(SpiDtoQuery query) {
@Nullable
@Override
public T findDtoOne(SpiDtoQuery query) {
- DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query);
- try {
- request.initTransIfRequired();
- return extractUnique(request.findList());
- } finally {
- request.endTransIfRequired();
- }
+ return extractUnique(findDtoList(query));
}
/**
@@ -2155,11 +2208,11 @@ public void slowQueryCheck(long timeMicros, int rowCount, SpiQuery> query) {
@Override
public Set checkUniqueness(Object bean) {
- return checkUniqueness(bean, null);
+ return checkUniqueness(bean, null, false, false);
}
@Override
- public Set checkUniqueness(Object bean, @Nullable Transaction transaction) {
+ public Set checkUniqueness(Object bean, @Nullable Transaction transaction, boolean useQueryCache, boolean skipClean) {
EntityBean entityBean = checkEntityBean(bean);
BeanDescriptor> beanDesc = descriptor(entityBean.getClass());
BeanProperty idProperty = beanDesc.idProperty();
@@ -2167,17 +2220,18 @@ public Set checkUniqueness(Object bean, @Nullable Transaction transact
if (idProperty == null) {
return Collections.emptySet();
}
- Object id = idProperty.value(entityBean);
+ Object id = idProperty.getValue(entityBean);
if (entityBean._ebean_getIntercept().isNew() && id != null) {
// Primary Key is changeable only on new models - so skip check if we are not new
Query> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
+ query.setUseQueryCache(useQueryCache);
query.setId(id);
- if (findCount(query, transaction) > 0) {
+ if (exists(query, transaction)) {
return Collections.singleton(idProperty);
}
}
for (BeanProperty[] props : beanDesc.uniqueProps()) {
- Set ret = checkUniqueness(entityBean, beanDesc, props, transaction);
+ Set ret = checkUniqueness(entityBean, beanDesc, props, transaction, useQueryCache, skipClean);
if (ret != null) {
return ret;
}
@@ -2185,26 +2239,47 @@ public Set checkUniqueness(Object bean, @Nullable Transaction transact
return Collections.emptySet();
}
+ /**
+ * Checks, if any property is dirty.
+ */
+ private boolean isAnyPropertyDirty(EntityBean entityBean, BeanProperty[] props) {
+ if (entityBean._ebean_getIntercept().isNew()) {
+ return true;
+ }
+ for (BeanProperty prop : props) {
+ if (entityBean._ebean_getIntercept().isDirtyProperty(prop.propertyIndex())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Returns a set of properties if saving the bean will violate the unique constraints (defined by given properties).
*/
@Nullable
- private Set checkUniqueness(EntityBean entityBean, BeanDescriptor> beanDesc, BeanProperty[] props, @Nullable Transaction transaction) {
+ private Set checkUniqueness(EntityBean entityBean, BeanDescriptor> beanDesc, BeanProperty[] props, @Nullable Transaction transaction,
+ boolean useQueryCache, boolean skipClean) {
+ if (skipClean && !isAnyPropertyDirty(entityBean, props)) {
+ return null;
+ }
+
BeanProperty idProperty = beanDesc.idProperty();
Query> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
+ query.setUseQueryCache(useQueryCache);
ExpressionList> exprList = query.where();
if (!entityBean._ebean_getIntercept().isNew()) {
// if model is not new, exclude ourself.
- exprList.ne(idProperty.name(), idProperty.value(entityBean));
+ exprList.ne(idProperty.name(), idProperty.getValue(entityBean));
}
- for (Property prop : props) {
- Object value = prop.value(entityBean);
+ for (BeanProperty prop : props) {
+ Object value = prop.getValue(entityBean);
if (value == null) {
return null;
}
exprList.eq(prop.name(), value);
}
- if (findCount(query, transaction) > 0) {
+ if (exists(query, transaction)) {
Set ret = new LinkedHashSet<>();
Collections.addAll(ret, props);
return ret;
@@ -2228,6 +2303,9 @@ public void visitMetrics(MetricVisitor visitor) {
persister.visitMetrics(visitor);
}
extraMetrics.visitMetrics(visitor);
+ for (Metric metric : MetricRegistry.registered()) {
+ metric.visit(visitor);
+ }
visitor.visitEnd();
}
@@ -2246,4 +2324,9 @@ List queryPlanInit(QueryPlanInit initRequest) {
List queryPlanCollectNow(QueryPlanRequest request) {
return queryPlanManager.collect(request);
}
+
+ @Override
+ public void runDdl() {
+ ddlGenerator.runDdl();
+ }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java
index 58c5dae77f..81e20f3c05 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java
@@ -4,7 +4,6 @@
import io.ebean.core.type.DataReader;
import io.ebeaninternal.api.SpiDtoQuery;
import io.ebeaninternal.api.SpiEbeanServer;
-import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.server.dto.DtoColumn;
import io.ebeaninternal.server.dto.DtoMappingRequest;
import io.ebeaninternal.server.dto.DtoQueryPlan;
@@ -31,11 +30,13 @@ public final class DtoQueryRequest extends AbstractSqlQueryRequest {
private final DtoQueryEngine queryEngine;
private DtoQueryPlan plan;
private DataReader dataReader;
+ private SpiOrmQueryRequest> ormRequest;
- DtoQueryRequest(SpiEbeanServer server, DtoQueryEngine engine, SpiDtoQuery query) {
+ DtoQueryRequest(SpiEbeanServer server, DtoQueryEngine engine, SpiDtoQuery query, SpiOrmQueryRequest> ormRequest) {
super(server, query, query.getTransaction());
this.queryEngine = engine;
this.query = query;
+ this.ormRequest = ormRequest;
query.obtainLocation();
}
@@ -43,20 +44,16 @@ public final class DtoQueryRequest extends AbstractSqlQueryRequest {
* Prepare and execute the SQL using the Binder.
*/
@Override
- public void executeSql(Binder binder, SpiQuery.Type type) throws SQLException {
+ public void executeSql(Binder binder) throws SQLException {
startNano = System.nanoTime();
- SpiQuery> ormQuery = query.getOrmQuery();
- if (ormQuery != null) {
- ormQuery.setType(type);
- ormQuery.setManualId();
-
- query.setCancelableQuery(ormQuery);
+ if (ormRequest != null) {
// execute the underlying ORM query returning the ResultSet
- SpiResultSet result = server.findResultSet(ormQuery, transaction);
+ query.setCancelableQuery(query.getOrmQuery());
+ ormRequest.transaction(transaction);
+ SpiResultSet result = ormRequest.findResultSet();
this.pstmt = result.getStatement();
- this.sql = ormQuery.getGeneratedSql();
- setResultSet(result.getResultSet(), ormQuery.getQueryPlanKey());
-
+ this.sql = ormRequest.query().getGeneratedSql();
+ setResultSet(result.getResultSet(), ormRequest.query().getQueryPlanKey());
} else {
// native SQL query execution
executeAsSql(binder);
@@ -155,4 +152,18 @@ static String parseColumn(String columnLabel) {
return columnLabel;
}
+ public List getFromQueryCache() {
+ if (ormRequest != null) {
+ return ormRequest.getFromQueryCache();
+ } else {
+ return null;
+ }
+ }
+
+ public void putToQueryCache(List result) {
+ if (ormRequest != null && ormRequest.isQueryCachePut()) {
+ ormRequest.putToQueryCache(result);
+ }
+ }
+
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DumpMetrics.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DumpMetrics.java
index c13a7d6dbe..4df3ce4ed9 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DumpMetrics.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DumpMetrics.java
@@ -7,6 +7,12 @@
import io.ebean.meta.SortMetric;
import io.ebeaninternal.api.SpiEbeanServer;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.List;
@@ -19,6 +25,7 @@ final class DumpMetrics {
private boolean dumpHash;
private boolean dumpSql;
private boolean dumpLoc;
+ private boolean dumpHtml;
private Comparator sortBy = SortMetric.NAME;
@@ -32,6 +39,7 @@ final class DumpMetrics {
dumpLoc = options.contains("loc");
dumpSql = options.contains("sql");
dumpHash = options.contains("hash");
+ dumpHtml = options.contains("html");
for (int i = 5; i < 10; i++) {
width = Math.max(width, optionWidth(i * 10));
}
@@ -72,7 +80,16 @@ private Comparator setSortOption(String option) {
}
void dump() {
-
+ if (dumpHtml) {
+ File file = new File("metric-report-" + server.name() + "-"
+ + DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss.SSS").format(LocalDateTime.now()) + ".html");
+ try (OutputStream out = new FileOutputStream(file)) {
+ server.metaInfo().createReportGenerator().writeReport(out);
+ out("html report written to: " + file.getAbsolutePath() + "\n");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
out("-- Dumping metrics for " + server.name() + " -- ");
ServerMetrics serverMetrics = server.metaInfo().collectMetrics();
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/HtmlMetricReportGenerator.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/HtmlMetricReportGenerator.java
new file mode 100644
index 0000000000..8f213f3e0f
--- /dev/null
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/HtmlMetricReportGenerator.java
@@ -0,0 +1,665 @@
+package io.ebeaninternal.server.core;
+
+import io.ebean.Database;
+import io.ebean.annotation.Platform;
+import io.ebean.cache.ServerCacheManager;
+import io.ebean.config.dbplatform.DatabasePlatform;
+import io.ebean.core.type.ScalarTypeUtils;
+import io.ebean.meta.*;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * HtmlMetricReportGenerator provides a neat interface for ebean metrics.
+ *
+ * @author Roland Praml, FOCONIS AG
+ */
+public class HtmlMetricReportGenerator implements MetricReportGenerator {
+
+ private static final Pattern SPLITPATTERN = Pattern.compile("[\\._]");
+ // pattern for MariaDB binary UUIDs
+ private static final Pattern BINPATTERN = Pattern.compile("\\[.*\\[(-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), "
+ + "(-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), "
+ + "(-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3}), (-?\\d{1,3})\\].*\\]");
+
+ private final DatabasePlatform platform;
+ private final MetaInfoManager metaInfo;
+ private final ServerCacheManager cacheManager;
+ private final QueryPlanRequest queryRequest = new QueryPlanRequest();
+ private final QueryPlanInit initRequest = new QueryPlanInit();
+ private final String name;
+ private List queryPlans = Collections.emptyList();
+
+ /**
+ * Initializes the report with some common defaults.
+ */
+ public HtmlMetricReportGenerator(Database db) {
+ platform = db.pluginApi().databasePlatform();
+ metaInfo = db.metaInfo();
+ cacheManager = db.cacheManager();
+ initRequest.thresholdMicros(100_000);
+ initRequest.setAll(true);
+ queryRequest.maxCount(10);
+ queryRequest.maxTimeMillis(30_000);
+ name = db.name();
+ }
+
+ /**
+ * This is used to configure the current report.
+ * It will receive the REST calls from the web application and returns either OK or RELOAD if the page should be reloaded.
+ */
+ @Override
+ public synchronized String configure(List configurations) {
+ String ret = "OK";
+ for (MetricReportValue configuration : configurations) {
+
+ if (configuration.getName().startsWith("hash.")) {
+ String hash = configuration.getName().substring(5);
+ if (configuration.intValue() > 0) {
+ initRequest.hashes().add(hash);
+ } else {
+ initRequest.hashes().remove(hash);
+ }
+ if (initRequest.isAll()) {
+ initRequest.setAll(false);
+ ret = "REFRESH";
+ }
+
+ } else {
+ switch (configuration.getName()) {
+ case "queryRequest.maxCount":
+ queryRequest.maxCount(configuration.intValue());
+ break;
+
+ case "queryRequest.maxTimeMillis":
+ queryRequest.maxTimeMillis(configuration.intValue());
+ break;
+
+ case "queryRequest.since":
+ queryRequest.since(configuration.intValue());
+ break;
+
+ case "queryRequest.apply":
+ queryPlans = metaInfo.queryPlanCollectNow(queryRequest);
+ ret = "REFRESH";
+ break;
+
+ case "initRequest.thresholdMicros":
+ initRequest.thresholdMicros(configuration.intValue());
+ break;
+
+ case "initRequest.isAll":
+ if (configuration.intValue() > 0) {
+ initRequest.hashes().clear();
+ }
+ initRequest.setAll(configuration.intValue() > 0);
+ break;
+
+ case "initRequest.apply":
+ queryPlans = metaInfo.queryPlanInit(initRequest);
+ ret = "REFRESH";
+ break;
+
+ case "clearCaches":
+ cacheManager.clearAll();
+ ret = "REFRESH";
+ break;
+
+ case "resetMetrics":
+ metaInfo.resetAllMetrics();
+ ret = "REFRESH";
+ break;
+
+ default:
+ throw new IllegalArgumentException(configuration.getName() + " is invalid");
+
+ }
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Writes the report as UTF-8 to the outputstream.
+ */
+ @Override
+ public synchronized void writeReport(OutputStream out) throws IOException {
+ OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
+ Html html = new Html();
+ createTabs(html);
+ StringBuilder sb = new StringBuilder(60
+ );
+ sb.append("Ebean metrics report for ");
+ addText(sb, name); // prevent HTML injection in name ;)
+ html.write(writer, sb.toString());
+ writer.flush();
+ }
+
+ /**
+ * Create tabs. Can be overwritten to create additional tabs.
+ */
+ protected void createTabs(Html html) {
+ // by default, we want to collect all metrics, but do not want to reset them
+ BasicMetricVisitor bmv = new BasicMetricVisitor(name, MetricNamingMatch.INSTANCE, false, true, true, true);
+ metaInfo.visitMetrics(bmv);
+ actionTab(html);
+ queryMetricTab(bmv.queryMetrics(), html);
+ timedMetricTab(bmv.timedMetrics(), html);
+ countMetricTab(bmv.countMetrics(), html);
+ queryPlansTab(html);
+ }
+
+ /**
+ * 1st tab: Action tab.
+ */
+ protected void actionTab(Html html) {
+ HtmlTab tab;
+
+ tab = html.tab("Actions");
+ tab.startTable("Name", "Value");
+ tab.input("initRequest.thresholdMicros", initRequest.thresholdMicros());
+ tab.input("initRequest.isAll", initRequest.isAll() ? 1 : 0);
+ tab.action("initRequest.apply", "Start capturing");
+
+ tab.input("queryRequest.maxCount", queryRequest.maxCount());
+ tab.input("queryRequest.maxTimeMillis", queryRequest.maxTimeMillis());
+ tab.input("queryRequest.since", queryRequest.since());
+ tab.tableRow("current time", System.currentTimeMillis());
+ tab.action("queryRequest.apply", "Collect plans");
+
+
+ tab.action("clearCaches", "Clear caches");
+ tab.action("resetMetrics", "Reset all metrics");
+
+ tab.endTable();
+ tab.html("Usage: ");
+ tab.html("\n");
+ tab.html("Set initRequest.thresholdMicros to the value, you want to capture \n");
+ tab.html("Add hashes or set initRequest.isAll = 1 (default) \n");
+ tab.html("Click 'Start capturing' \n");
+ tab.html("Do the action, which executes the query/queries you want to inspect \n");
+ tab.html("Click 'Collect plans' to collect the query-plans \n");
+ tab.html(" \n");
+ }
+
+ /**
+ * 2nd tab: Query Metrics.
+ */
+ protected void queryMetricTab(List metrics, Html html) {
+
+ HtmlTab tab = html.tab("Query metrics");
+ tab.startTable("type?", "?", "?", "?", "count", "total", "mean", "max", "sql", "hash");
+ for (MetaQueryMetric metric : metrics) {
+ String[] names = splitPad(metric.name(), 4);
+ tab.tableRow(names[0], names[1], names[2], names[3],
+ metric.count(),
+ micros(metric.total()), micros(metric.mean()), micros(metric.max()),
+ metric.sql(), hash(metric.hash()));
+ }
+ tab.endTable();
+ }
+
+ /**
+ * 3rd tab: Timed metrics.
+ */
+ protected void timedMetricTab(List metrics, Html html) {
+ HtmlTab tab = html.tab("Timed metrics");
+ tab.startTable("type?", "?", "?", "?", "count", "total", "mean", "max");
+ for (MetaTimedMetric metric : metrics) {
+ String[] names = splitPad(metric.name(), 4);
+ tab.tableRow(names[0], names[1], names[2], names[3],
+ metric.count(),
+ micros(metric.total()), micros(metric.mean()), micros(metric.max()));
+ }
+ tab.endTable();
+ }
+
+ /**
+ * 4th tab: Count metrics.
+ */
+ protected void countMetricTab(List metrics, Html html) {
+ HtmlTab tab = html.tab("Count Metrics");
+ tab.startTable("type?", "?", "?", "?", "count");
+ for (MetaCountMetric metric : metrics) {
+ String[] names = splitPad(metric.name(), 4);
+ tab.tableRow(names[0], names[1], names[2], names[3], metric.count());
+ }
+ tab.endTable();
+ }
+
+ /**
+ * 5th tab: Query plans.
+ */
+ protected void queryPlansTab(Html html) {
+ HtmlTab tab = html.tab("Query plans");
+ tab.startTable("type?", "tenant?", "count", "micros", "sql", "whenCaptured", "captureMicros","hash");
+ for (MetaQueryPlan queryPlan : queryPlans) {
+ tab.tableRow(queryPlan.beanType().getSimpleName(),
+ queryPlan.tenantId(),
+ queryPlan.captureCount(),
+ micros(queryPlan.queryTimeMicros()),
+ queryPlan(queryPlan.sql(), queryPlan.bind(), queryPlan.plan()),
+ queryPlan.whenCaptured(),
+ queryPlan.captureMicros(),
+ hash(queryPlan.hash()));
+ }
+ tab.endTable();
+ }
+
+ /**
+ * Splits the string on '.' or '_' with a fixed length
of entries.
+ */
+ protected static String[] splitPad(String name, int length) {
+ String[] ret = SPLITPATTERN.split(name, length);
+ if (ret.length < length) {
+ return Arrays.copyOf(ret, length);
+ }
+ return ret;
+ }
+
+ /**
+ * Adds html-safe text to the string-builder.
+ */
+ protected static void addText(StringBuilder sb, String text) {
+ for (int i = 0; i < text.length(); i++) {
+ char ch = text.charAt(i);
+
+ switch (ch) {
+ case '<':
+ sb.append("<");
+ break;
+ case '>':
+ sb.append(">");
+ break;
+ case '"':
+ sb.append(""");
+ break;
+ case '&':
+ sb.append("&");
+ break;
+ case '\'':
+ sb.append("'");
+ break; // HTML entity for Apostroph '
+ default:
+ sb.append(ch);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Creates a QueryPlan object. This is rendered as sql and optionally displayable bind and plan.
+ */
+ protected QueryPlan queryPlan(String sql, String bind, String plan) {
+
+ if (bind != null && (platform.isPlatform(Platform.MARIADB) || platform.isPlatform(Platform.MYSQL))) {
+ // MariaDB UUIDs beatifier.
+ Matcher matcher = BINPATTERN.matcher(bind);
+
+ while (matcher.matches()) {
+ byte[] bytes = new byte[16];
+ for (int i = 0; i < 16; i++) {
+ bytes[i] = Byte.parseByte(matcher.group(i + 1));
+ }
+ UUID uuid = ScalarTypeUtils.uuidFromBytes(bytes, true);
+ bind = bind.substring(0, matcher.start(1) - 1) + uuid + bind.substring(matcher.end(16) + 1);
+ matcher = BINPATTERN.matcher(bind);
+ }
+ }
+ return new QueryPlan(sql, bind, plan, platform.isPlatform(Platform.SQLSERVER));
+ }
+
+ /**
+ * Creates a Micros object. It is rendered in human readable form. e.g. "12345678" micros is converted to "12.3 s"
+ */
+ protected Micros micros(long value) {
+ return new Micros(value);
+ }
+
+ /**
+ * Creates a Hash object. It is rendered with a checkbox.
+ */
+ protected Hash hash(String hash) {
+ return new Hash(hash, initRequest.includeHash(hash));
+ }
+
+ /**
+ * Returns the currently collected plans. Maybe useful, if you want to fetch them via JSON.
+ */
+ public List getCurrentPlans() {
+ return queryPlans;
+ }
+
+ /**
+ * Human readable micros object.
+ */
+ protected static class Micros {
+ private final long micros;
+
+ protected Micros(long micros) {
+ this.micros = micros;
+ }
+
+ @Override
+ public String toString() {
+ if (micros < 1_000L) {
+ return String.format("%d µs", micros);
+ } else if (micros < 10_000L) {
+ return String.format("%.2f ms", micros / 1000d);
+ } else if (micros < 100_000L) {
+ return String.format("%.1f ms", micros / 1000d);
+ } else if (micros < 1_000_000L) {
+ return String.format("%.0f ms", micros / 1000d);
+ } else if (micros < 10_000_000L) {
+ return String.format("%.2f s", micros / 1000_000d);
+ } else if (micros < 100_000_000L) {
+ return String.format("%.1f s", micros / 1000_000d);
+ } else {
+ return String.format("%.0f s", micros / 1000_000d);
+ }
+ }
+ }
+
+ /**
+ * QueryPan popup object.
+ */
+ protected static class QueryPlan {
+ final String sql;
+ final String bind;
+ final String plan;
+ final boolean sqlServer;
+
+ QueryPlan(String sql, String bind, String plan, boolean sqlServer) {
+ this.sql = sql;
+ this.bind = bind;
+ this.plan = plan;
+ this.sqlServer = sqlServer;
+ }
+ }
+
+ /**
+ * Hash object.
+ */
+ public static class Hash {
+ final String hash;
+ final boolean checked;
+
+ Hash(String hash, boolean checked) {
+ this.hash = hash;
+ this.checked = checked;
+ }
+
+ @Override
+ public String toString() {
+ return hash;
+ }
+ }
+
+ /**
+ * This class reprensets the whole HTML plage with it's tabs.
+ */
+ public static class Html {
+
+ private final List tabs = new ArrayList<>();
+
+ public HtmlTab tab(String title) {
+ HtmlTab htmlTab = new HtmlTab(title);
+ tabs.add(htmlTab);
+ return htmlTab;
+ }
+
+ protected void appendResource(Appendable out, String resName) throws IOException {
+ InputStream res = getClass().getResourceAsStream(resName);
+ if (res == null) {
+ throw new NullPointerException("Could not find " + resName);
+ }
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(res, StandardCharsets.UTF_8))) {
+ Iterator it = reader.lines().iterator();
+ while (it.hasNext()) {
+ out.append(it.next()).append('\n');
+ }
+ }
+ }
+
+ public void write(Appendable out, String title) throws IOException {
+ out.append("\n");
+ out.append("\n");
+ out.append("");
+ out.append(title);
+ out.append(" ");
+ //sb.append(" ");
+ out.append("");
+ out.append("\n");
+ out.append("");
+ out.append("");
+ out.append(title);
+ out.append(" ");
+ out.append(" display raw values ");
+ out.append(" display query plan bind values ");
+ out.append(" display query plan details ");
+ out.append("\n");
+ // add radio inputs that controls the selected tabs.
+ for (int i = 0; i < tabs.size(); i++) {
+ out.append("
\n");
+ }
+
+ // add labels (=tabs)
+ out.append("
\n");
+ for (int i = 0; i < tabs.size(); i++) {
+ out.append(" ");
+ out.append(tabs.get(i).getTitle()); // HTML in title would be allowed
+ out.append(" \n");
+ }
+ out.append(" \n");
+
+ // add tab contents
+ out.append("
\n");
+ for (int i = 0; i < tabs.size(); i++) {
+ out.append(" ");
+ out.append(tabs.get(i).toString());
+ out.append("
\n");
+ }
+ out.append(" \n");
+ out.append("
\n");
+ // add javascript.
+ out.append("");
+ }
+
+ /**
+ * Adds css for each tab by replacing the '@' sign with tab numbers.
+ */
+ protected void addTabCss(Appendable sb, String template, String css) throws IOException {
+ for (int i = 0; i < tabs.size(); i++) {
+ if (i > 0) {
+ sb.append(", ");
+ }
+ sb.append(template.replace("@", String.valueOf(i)));
+ }
+ sb.append(css);
+ sb.append('\n');
+ }
+ }
+
+ /**
+ * HtmlTab that is mainly used to render tables.
+ */
+ public static class HtmlTab {
+
+ private final String title;
+
+ private final StringBuilder sb = new StringBuilder();
+
+ HtmlTab(String title) {
+ this.title = title;
+ }
+
+ /**
+ * Return the table's title.
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Adds table headers. If header ends with '?' - a filter will be added.
+ */
+ public void startTable(String... headers) {
+ sb.append("\n");
+ sb.append("\n");
+ sb.append("");
+
+ boolean hasFilter = false;
+ for (String header : headers) {
+ sb.append("");
+ addText(sb, header);
+ if (header.endsWith("?")) {
+ sb.setLength(sb.length() - 1);
+ hasFilter = true;
+ }
+ sb.append(" \n");
+ }
+ sb.append(" ");
+ if (hasFilter) {
+ // add second header row for filters
+ sb.append("");
+ for (String header : headers) {
+ sb.append("");
+ if (header.endsWith("?")) {
+ sb.append(" ");
+ } else {
+ sb.append(" ");
+ }
+ sb.append(" \n");
+ }
+ sb.append(" ");
+ }
+ sb.append(" \n");
+ }
+
+ public void tableRow(Object... items) {
+ sb.append("");
+
+ for (Object item : items) {
+ if (item == null) {
+ sb.append(" ");
+
+ } else if (item instanceof QueryPlan) {
+ // Render the QueryPlan sql, bind and plan output.
+ sb.append("");
+ QueryPlan queryPlan = (QueryPlan) item;
+ addText(sb, String.valueOf(queryPlan.sql));
+ if (queryPlan.bind != null) {
+ sb.append("");
+ addText(sb, queryPlan.bind);
+ sb.append(" ");
+ }
+ if (queryPlan.plan != null) {
+ sb.append("");
+ if (queryPlan.sqlServer) {
+ sb.append("Download ");
+ } else {
+ addText(sb, String.valueOf(queryPlan.plan));
+ }
+ sb.append("
");
+ }
+ sb.append(" ");
+
+ } else if (item instanceof Micros) {
+ sb.append("");
+ addText(sb, String.valueOf(item));
+ sb.append("
");
+ addText(sb, String.valueOf(((Micros) item).micros));
+ sb.append("
");
+
+ } else if (item instanceof Number) {
+ sb.append("");
+ addText(sb, String.valueOf(item));
+ sb.append(" ");
+
+ } else if (item instanceof Hash) {
+ Hash hash = (Hash) item;
+ sb.append("");
+ sb.append(" ").append(hash.hash).append(" ");
+
+ } else {
+ sb.append("");
+ addText(sb, String.valueOf(item));
+ sb.append(" ");
+
+ }
+ }
+ sb.append(" \n");
+ }
+
+ /**
+ * Adds an input field to the table. Changes are sent to the updateValue
javascript function.
+ */
+ public void input(String label, long value) {
+ sb.append("");
+ addText(sb, label);
+ sb.append(" ");
+ sb.append(" \n");
+ }
+
+ /**
+ * Adds an action button to the table. Changes are sent to the updateValue
javascript function.
+ */
+ public void action(String actionId, String caption) {
+ sb.append(" ");
+ sb.append("");
+ addText(sb, caption);
+ sb.append(" \n");
+ }
+
+ public void endTable() {
+ // add a second tbody. it is used as buffer for filtered entries
+ sb.append("
\n");
+ }
+
+ /**
+ * Add HTML to this tab.
+ */
+ public void html(String html) {
+ sb.append(html);
+ }
+
+ @Override
+ public String toString() {
+ return sb.toString();
+ }
+ }
+}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
index 080d95560c..2d61e024c4 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
@@ -4,10 +4,7 @@
import io.ebean.ExpressionFactory;
import io.ebean.annotation.Platform;
import io.ebean.cache.*;
-import io.ebean.config.DatabaseConfig;
-import io.ebean.config.ExternalTransactionManager;
-import io.ebean.config.ProfilingConfig;
-import io.ebean.config.SlowQueryListener;
+import io.ebean.config.*;
import io.ebean.config.dbplatform.DatabasePlatform;
import io.ebean.config.dbplatform.DbHistorySupport;
import io.ebean.event.changelog.ChangeLogListener;
@@ -37,6 +34,7 @@
import io.ebeaninternal.server.expression.DefaultExpressionFactory;
import io.ebeaninternal.server.expression.platform.DbExpressionHandler;
import io.ebeaninternal.server.expression.platform.DbExpressionHandlerFactory;
+import io.ebeaninternal.server.json.DJsonContext;
import io.ebeaninternal.server.logger.DLogManager;
import io.ebeaninternal.server.logger.DLoggerFactory;
import io.ebeaninternal.server.persist.Binder;
@@ -46,7 +44,6 @@
import io.ebeaninternal.server.query.*;
import io.ebeaninternal.server.readaudit.DefaultReadAuditLogger;
import io.ebeaninternal.server.readaudit.DefaultReadAuditPrepare;
-import io.ebeaninternal.server.json.DJsonContext;
import io.ebeaninternal.server.transaction.*;
import io.ebeaninternal.server.type.DefaultTypeManager;
import io.ebeaninternal.server.type.TypeManager;
@@ -76,9 +73,11 @@ public final class InternalConfiguration {
private final DatabasePlatform databasePlatform;
private final DeployInherit deployInherit;
private final TypeManager typeManager;
+ private final TempFileProvider tempFileProvider;
private final DtoBeanManager dtoBeanManager;
private final ClockService clockService;
private final DataTimeZone dataTimeZone;
+ private final int maxStringSize;
private final Binder binder;
private final DeployCreateProperties deployCreateProperties;
private final DeployUtil deployUtil;
@@ -116,6 +115,7 @@ public final class InternalConfiguration {
this.databasePlatform = config.getDatabasePlatform();
this.expressionFactory = initExpressionFactory(config);
this.typeManager = new DefaultTypeManager(config, bootupClasses);
+ this.tempFileProvider = config.getTempFileProvider();
this.multiValueBind = createMultiValueBind(databasePlatform.platform());
this.deployInherit = new DeployInherit(bootupClasses);
this.deployCreateProperties = new DeployCreateProperties(typeManager);
@@ -130,7 +130,8 @@ public final class InternalConfiguration {
Map draftTableMap = beanDescriptorManager.draftTableMap();
beanDescriptorManager.scheduleBackgroundTrim();
this.dataTimeZone = initDataTimeZone();
- this.binder = getBinder(typeManager, databasePlatform, dataTimeZone);
+ this.maxStringSize = config.getMaxStringSize();
+ this.binder = getBinder(typeManager, databasePlatform, dataTimeZone, maxStringSize);
this.cQueryEngine = new CQueryEngine(config, databasePlatform, binder, asOfTableMapping, draftTableMap);
}
@@ -266,15 +267,15 @@ ReadAuditPrepare getReadAuditPrepare() {
/**
* For 'As Of' queries return the number of bind variables per predicate.
*/
- private Binder getBinder(TypeManager typeManager, DatabasePlatform databasePlatform, DataTimeZone dataTimeZone) {
+ private Binder getBinder(TypeManager typeManager, DatabasePlatform databasePlatform, DataTimeZone dataTimeZone, int maxStringSize) {
DbExpressionHandler jsonHandler = getDbExpressionHandler(databasePlatform);
DbHistorySupport historySupport = databasePlatform.historySupport();
if (historySupport == null) {
- return new Binder(typeManager, logManager, 0, false, jsonHandler, dataTimeZone, multiValueBind);
+ return new Binder(typeManager, logManager, 0, false, jsonHandler, dataTimeZone, maxStringSize, multiValueBind);
}
- return new Binder(typeManager, logManager, historySupport.getBindCount(), historySupport.isStandardsBased(), jsonHandler, dataTimeZone, multiValueBind);
+ return new Binder(typeManager, logManager, historySupport.getBindCount(), historySupport.isStandardsBased(), jsonHandler, dataTimeZone, maxStringSize, multiValueBind);
}
/**
@@ -472,6 +473,10 @@ public DataTimeZone getDataTimeZone() {
return dataTimeZone;
}
+ public int getMaxStringSize() {
+ return maxStringSize;
+ }
+
public ServerCacheManager cacheManager() {
return new DefaultCacheAdapter(cacheManager);
}
@@ -517,6 +522,10 @@ SpiLogManager getLogManager() {
return logManager;
}
+ public TempFileProvider getTempFileProvider() {
+ return tempFileProvider;
+ }
+
private ServerCachePlugin initServerCachePlugin() {
if (config.isLocalOnlyL2Cache()) {
localL2Caching = true;
@@ -582,7 +591,8 @@ public QueryPlanManager initQueryPlanManager(TransactionManager transactionManag
return QueryPlanManager.NOOP;
}
long threshold = config.getQueryPlanThresholdMicros();
- return new CQueryPlanManager(transactionManager, threshold, queryPlanLogger(databasePlatform.platform()), extraMetrics);
+ return new CQueryPlanManager(transactionManager, config.getCurrentTenantProvider(),
+ threshold, queryPlanLogger(databasePlatform.platform()), extraMetrics);
}
/**
@@ -596,6 +606,8 @@ QueryPlanLogger queryPlanLogger(Platform platform) {
return new QueryPlanLoggerSqlServer();
case ORACLE:
return new QueryPlanLoggerOracle();
+ case DB2:
+ return new QueryPlanLoggerDb2(config.getQueryPlanOptions());
default:
return new QueryPlanLoggerExplain();
}
@@ -616,6 +628,11 @@ private static class NoopDdl implements SpiDdlGenerator {
this.ddlRun = ddlRun;
}
+ @Override
+ public void runDdl() {
+ CoreLog.log.log(ERROR, "Manual DDL run not possible");
+ }
+
@Override
public void execute(boolean online) {
if (online && ddlRun) {
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java
index 6f0930523c..b80601c72a 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java
@@ -40,11 +40,13 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery
private PersistenceContext persistenceContext;
private HashQuery cacheKey;
private CQueryPlanKey queryPlanKey;
+ // The queryPlan during the request.
+ private CQueryPlan queryPlan;
private SpiQuerySecondary secondaryQueries;
private List cacheBeans;
private BeanPropertyAssocMany> manyProperty;
private boolean inlineCountDistinct;
- private Set dependentTables;
+ private boolean prepared;
public OrmQueryRequest(SpiEbeanServer server, OrmQueryEngine queryEngine, SpiQuery query, SpiTransaction t) {
super(server, t);
@@ -68,6 +70,7 @@ public boolean isDeleteByStatement() {
} else {
// delete by ids due to cascading delete needs
queryPlanKey = query.setDeleteByIdsPlan();
+ queryPlan = null;
return false;
}
}
@@ -172,10 +175,24 @@ private void adapterPreQuery() {
*/
@Override
public void prepareQuery() {
- secondaryQueries = query.convertJoins();
- beanDescriptor.prepareQuery(query);
- adapterPreQuery();
- queryPlanKey = query.prepare(this);
+ if (!prepared) {
+ secondaryQueries = query.convertJoins();
+ beanDescriptor.prepareQuery(query);
+ adapterPreQuery();
+ queryPlanKey = query.prepare(this);
+ prepared = true;
+ }
+
+ }
+
+ /**
+ * The queryPlanKey has to be updated, if elements are removed from an already prepared query.
+ */
+ private void updateQueryPlanKey() {
+ if (prepared) {
+ queryPlanKey = query.prepare(this);
+ queryPlan = null;
+ }
}
public boolean isNativeSql() {
@@ -470,7 +487,10 @@ public BeanPropertyAssocMany> manyPropertyForOrderBy() {
* query plan for this query exists.
*/
public CQueryPlan queryPlan() {
- return beanDescriptor.queryPlan(queryPlanKey);
+ if (queryPlan == null) {
+ queryPlan = beanDescriptor.queryPlan(queryPlanKey);
+ }
+ return queryPlan;
}
/**
@@ -488,6 +508,7 @@ public CQueryPlanKey queryPlanKey() {
* Put the QueryPlan into the cache.
*/
public void putQueryPlan(CQueryPlan queryPlan) {
+ this.queryPlan = queryPlan;
beanDescriptor.queryPlan(queryPlanKey, queryPlan);
}
@@ -496,8 +517,16 @@ public void resetBeanCacheAutoMode(boolean findOne) {
query.resetBeanCacheAutoMode(findOne);
}
+ @Override
+ public boolean isQueryCacheActive() {
+ return query.getUseQueryCache() != CacheMode.OFF
+ && (transaction == null || !transaction.isSkipCache())
+ && !server.isDisableL2Cache();
+ }
+
+ @Override
public boolean isQueryCachePut() {
- return cacheKey != null && query.getUseQueryCache().isPut();
+ return cacheKey != null && queryPlan != null && query.getUseQueryCache().isPut();
}
public boolean isBeanCachePutMany() {
@@ -606,7 +635,14 @@ public boolean getFromBeanCache() {
BeanCacheResult cacheResult = beanDescriptor.cacheIdLookup(persistenceContext, idLookup.idValues());
// adjust the query (IN clause) based on the cache hits
this.cacheBeans = idLookup.removeHits(cacheResult);
- return idLookup.allHits();
+ if (idLookup.allHits()) {
+ return true;
+ } else {
+ if (!this.cacheBeans.isEmpty()) {
+ updateQueryPlanKey();
+ }
+ return false;
+ }
}
if (!beanDescriptor.isNaturalKeyCaching()) {
return false;
@@ -619,7 +655,14 @@ public boolean getFromBeanCache() {
BeanCacheResult cacheResult = beanDescriptor.naturalKeyLookup(persistenceContext, naturalKeySet.keys());
// adjust the query (IN clause) based on the cache hits
this.cacheBeans = data.removeHits(cacheResult);
- return data.allHits();
+ if (data.allHits()) {
+ return true;
+ } else {
+ if (!this.cacheBeans.isEmpty()) {
+ updateQueryPlanKey();
+ }
+ return false;
+ }
}
}
return false;
@@ -631,13 +674,12 @@ public boolean getFromBeanCache() {
@Override
@SuppressWarnings("unchecked")
public Object getFromQueryCache() {
- if (query.getUseQueryCache() == CacheMode.OFF
- || (transaction != null && transaction.isSkipCache())
- || server.isDisableL2Cache()) {
+ if (!isQueryCacheActive()) {
return null;
} else {
cacheKey = query.queryHash();
}
+ // check if queryCache is active and put-only
if (!query.getUseQueryCache().isGet()) {
return null;
}
@@ -687,8 +729,9 @@ private boolean readAuditQueryType() {
}
}
+ @Override
public void putToQueryCache(Object result) {
- beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, dependentTables, transaction.getStartNanoTime()));
+ beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, queryPlan.dependentTables(), transaction.getStartNanoTime()));
}
/**
@@ -758,15 +801,6 @@ public boolean isInlineCountDistinct() {
return inlineCountDistinct;
}
- public void addDependentTables(Set tables) {
- if (tables != null && !tables.isEmpty()) {
- if (dependentTables == null) {
- dependentTables = new LinkedHashSet<>();
- }
- dependentTables.addAll(tables);
- }
- }
-
/**
* Return true if no MaxRows or use LIMIT in SQL update.
*/
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java
index f9b08aa5fb..d06369da8b 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java
@@ -272,24 +272,30 @@ private void onUpdateGeneratedProperties() {
} else {
// @WhenModified set without invoking interception
Object oldVal = prop.getValue(entityBean);
- Object value = generatedProperty.getUpdateValue(prop, entityBean, now());
- prop.setValueChanged(entityBean, value);
- intercept.setOldValue(prop.propertyIndex(), oldVal);
+ if (transaction == null || transaction.isOverwriteGeneratedProperties() || oldVal == null) { // version handled above
+ Object value = generatedProperty.getUpdateValue(prop, entityBean, now());
+ prop.setValueChanged(entityBean, value);
+ intercept.setOldValue(prop.propertyIndex(), oldVal);
+ }
}
}
}
private void onFailedUpdateUndoGeneratedProperties() {
for (BeanProperty prop : beanDescriptor.propertiesGenUpdate()) {
- Object oldVal = intercept.origValue(prop.propertyIndex());
- prop.setValue(entityBean, oldVal);
+ if (transaction == null || transaction.isOverwriteGeneratedProperties() || prop.isVersion()) {
+ Object oldVal = intercept.origValue(prop.propertyIndex());
+ prop.setValue(entityBean, oldVal);
+ }
}
}
private void onInsertGeneratedProperties() {
for (BeanProperty prop : beanDescriptor.propertiesGenInsert()) {
- Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now());
- prop.setValueChanged(entityBean, value);
+ if (transaction == null || transaction.isOverwriteGeneratedProperties() || prop.isVersion() || prop.getValue(entityBean) == null) {
+ Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now());
+ prop.setValueChanged(entityBean, value);
+ }
}
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java
index ff2c0ef541..fdff9dc364 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java
@@ -135,6 +135,21 @@ public interface SpiOrmQueryRequest extends BeanQueryRequest, DocQueryRequ
*/
A getFromQueryCache();
+ /**
+ * Return if query cache is active.
+ */
+ boolean isQueryCacheActive();
+
+ /**
+ * Return if results should be put to query cache.
+ */
+ boolean isQueryCachePut();
+
+ /**
+ * Put the result to the query cache.
+ */
+ void putToQueryCache(Object result);
+
/**
* Maybe hit the bean cache returning true if everything was obtained from the
* cache (that there were no misses).
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java
index 9ab9ae4b5f..660644512e 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java
@@ -1,6 +1,7 @@
package io.ebeaninternal.server.core.bootup;
import io.ebean.annotation.DocStore;
+import io.ebean.bean.extend.EntityExtension;
import io.ebean.config.DatabaseConfig;
import io.ebean.config.IdGenerator;
import io.ebean.config.ScalarTypeConverter;
@@ -11,6 +12,7 @@
import io.ebean.event.changelog.ChangeLogRegister;
import io.ebean.event.readaudit.ReadAuditLogger;
import io.ebean.event.readaudit.ReadAuditPrepare;
+import io.ebean.plugin.CustomDeployParser;
import io.ebean.util.AnnotationUtil;
import io.ebeaninternal.api.CoreLog;
@@ -38,6 +40,7 @@ public class BootupClasses implements Predicate> {
private final List> embeddableList = new ArrayList<>();
private final List> entityList = new ArrayList<>();
+ private final List> entityExtensionList = new ArrayList<>();
private final List>> scalarTypeList = new ArrayList<>();
private final List>> scalarConverterList = new ArrayList<>();
private final List>> attributeConverterList = new ArrayList<>();
@@ -54,6 +57,7 @@ public class BootupClasses implements Predicate> {
private final List> beanPersistListenerCandidates = new ArrayList<>();
private final List