package com.avaje.ebeaninternal.server.deploy; import com.avaje.ebean.ValuePair; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.bean.PersistenceContext; import com.avaje.ebean.config.EncryptKey; import com.avaje.ebean.config.dbplatform.DbEncryptFunction; import com.avaje.ebean.config.dbplatform.DbPlatformType; import com.avaje.ebean.plugin.Property; import com.avaje.ebean.text.StringParser; import com.avaje.ebeaninternal.api.SpiExpressionRequest; import com.avaje.ebeaninternal.server.core.InternString; import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedProperty; import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedWhenCreated; import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedWhenModified; import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty; import com.avaje.ebeaninternal.server.el.ElPropertyChainBuilder; import com.avaje.ebeaninternal.server.el.ElPropertyValue; import com.avaje.ebeaninternal.server.lib.util.StringHelper; import com.avaje.ebeaninternal.server.properties.BeanPropertyGetter; import com.avaje.ebeaninternal.server.properties.BeanPropertySetter; import com.avaje.ebeaninternal.server.query.SqlBeanLoad; import com.avaje.ebeaninternal.server.query.SqlJoinType; import com.avaje.ebeaninternal.server.text.json.ReadJson; import com.avaje.ebeaninternal.server.text.json.WriteJson; import com.avaje.ebeaninternal.server.type.DataBind; import com.avaje.ebeaninternal.server.type.ScalarType; import com.avaje.ebeaninternal.server.type.ScalarTypeBoolean; import com.avaje.ebeaninternal.server.type.ScalarTypeEnum; import com.avaje.ebeaninternal.server.type.ScalarTypeLogicalType; import com.avaje.ebeaninternal.util.ValueUtil; import com.avaje.ebeanservice.docstore.api.mapping.DocMappingBuilder; import com.avaje.ebeanservice.docstore.api.mapping.DocPropertyMapping; import com.avaje.ebeanservice.docstore.api.mapping.DocPropertyOptions; import com.avaje.ebeanservice.docstore.api.mapping.DocPropertyType; import com.avaje.ebeanservice.docstore.api.support.DocStructure; import com.fasterxml.jackson.core.JsonToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.persistence.PersistenceException; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.lang.reflect.Field; import java.sql.SQLException; import java.sql.Types; import java.util.List; import java.util.Map; import java.util.Set; /** * Description of a property of a bean. Includes its deployment information such * as database column mapping information. */ public class BeanProperty implements ElPropertyValue, Property { private static final Logger logger = LoggerFactory.getLogger(BeanProperty.class); /** * Flag to mark this at part of the unique id. */ final boolean id; /** * Flag to make this as a dummy property for unidirecitonal relationships. */ final boolean unidirectionalShadow; /** * Flag set if this maps to the inheritance discriminator column */ final boolean discriminator; /** * Flag to mark the property as embedded. This could be on * BeanPropertyAssocOne rather than here. Put it here for checking Id type * (embedded or not). */ final boolean embedded; /** * Flag indicating if this the version property. */ final boolean version; final boolean naturalKey; /** * Set if this property is nullable. */ final boolean nullable; final boolean unique; /** * Is this property include in database resultSet. */ final boolean dbRead; /** * Include in DB insert. */ final boolean dbInsertable; /** * Include in DB update. */ final boolean dbUpdatable; /** * True if the property is based on a SECONDARY table. */ final boolean secondaryTable; final TableJoin secondaryTableJoin; final String secondaryTableJoinPrefix; /** * The property is inherited from a super class. */ final boolean inherited; final Class> owningType; final boolean local; /** * True if the property is a Clob, Blob LongVarchar or LongVarbinary. */ final boolean lob; final boolean fetchEager; final boolean isTransient; /** * The logical bean property name. */ final String name; final int propertyIndex; /** * The reflected field. */ final Field field; /** * The bean type. */ final Class> propertyType; final String dbBind; /** * The database column. This can include quoted identifiers. */ final String dbColumn; final String elPrefix; final String elPlaceHolder; final String elPlaceHolderEncrypted; /** * Select part of a SQL Formula used to populate this property. */ final String sqlFormulaSelect; /** * Join part of a SQL Formula. */ final String sqlFormulaJoin; final String aggregation; final boolean formula; /** * Set to true if stored encrypted. */ final boolean dbEncrypted; final boolean localEncrypted; final int dbEncryptedType; /** * The jdbc data type this maps to. */ final int dbType; final boolean excludedFromHistory; /** * Generator for insert or update timestamp etc. */ final GeneratedProperty generatedProperty; final BeanPropertyGetter getter; final BeanPropertySetter setter; final BeanDescriptor> descriptor; /** * Used for non-jdbc native types (java.util.Date Enums etc). Converts from * logical to jdbc types. */ @SuppressWarnings("rawtypes") final ScalarType scalarType; final DocPropertyOptions docOptions; /** * The length or precision for DB column. */ final int dbLength; /** * The scale for DB column (decimal). */ final int dbScale; /** * Deployment defined DB column definition. */ final String dbColumnDefn; /** * DB Column default value for DDL definition (FALSE, NOW etc). */ final String dbColumnDefault; /** * Database DDL column comment. */ final String dbComment; final DbEncryptFunction dbEncryptFunction; int deployOrder; final boolean jsonSerialize; final boolean jsonDeserialize; final boolean draft; final boolean draftOnly; final boolean draftDirty; final boolean draftReset; final boolean softDelete; final String softDeleteDbSet; final String softDeleteDbPredicate; public BeanProperty(DeployBeanProperty deploy) { this(null, deploy); } public BeanProperty(BeanDescriptor> descriptor, DeployBeanProperty deploy) { this.descriptor = descriptor; this.name = InternString.intern(deploy.getName()); this.propertyIndex = deploy.getPropertyIndex(); this.unidirectionalShadow = deploy.isUndirectionalShadow(); this.discriminator = deploy.isDiscriminator(); this.localEncrypted = deploy.isLocalEncrypted(); this.dbEncrypted = deploy.isDbEncrypted(); this.dbEncryptedType = deploy.getDbEncryptedType(); this.dbEncryptFunction = deploy.getDbEncryptFunction(); this.dbBind = deploy.getDbBind(); this.dbRead = deploy.isDbRead(); this.dbInsertable = deploy.isDbInsertable(); this.dbUpdatable = deploy.isDbUpdateable(); this.excludedFromHistory = deploy.isExcludedFromHistory(); this.draft = deploy.isDraft(); this.draftDirty = deploy.isDraftDirty(); this.draftOnly = deploy.isDraftOnly(); this.draftReset = deploy.isDraftReset(); this.secondaryTable = deploy.isSecondaryTable(); if (secondaryTable) { this.secondaryTableJoin = new TableJoin(deploy.getSecondaryTableJoin()); this.secondaryTableJoinPrefix = deploy.getSecondaryTableJoinPrefix(); } else { this.secondaryTableJoin = null; this.secondaryTableJoinPrefix = null; } this.fetchEager = deploy.isFetchEager(); this.isTransient = deploy.isTransient(); this.nullable = deploy.isNullable(); this.unique = deploy.isUnique(); this.naturalKey = deploy.isNaturalKey(); this.dbLength = deploy.getDbLength(); this.dbScale = deploy.getDbScale(); this.dbColumnDefn = InternString.intern(deploy.getDbColumnDefn()); this.dbColumnDefault = deploy.getDbColumnDefault(); this.inherited = false;// deploy.isInherited(); this.owningType = deploy.getOwningType(); this.local = deploy.isLocal(); this.version = deploy.isVersionColumn(); this.embedded = deploy.isEmbedded(); this.id = deploy.isId(); this.generatedProperty = deploy.getGeneratedProperty(); this.getter = deploy.getGetter(); this.setter = deploy.getSetter(); this.dbColumn = tableAliasIntern(descriptor, deploy.getDbColumn(), false, null); this.dbComment = deploy.getDbComment(); this.aggregation = deploy.getAggregation(); this.sqlFormulaJoin = InternString.intern(deploy.getSqlFormulaJoin()); this.sqlFormulaSelect = InternString.intern(deploy.getSqlFormulaSelect()); this.formula = sqlFormulaSelect != null; this.dbType = deploy.getDbType(); this.scalarType = deploy.getScalarType(); this.lob = isLobType(dbType); this.propertyType = deploy.getPropertyType(); this.field = deploy.getField(); this.docOptions = deploy.getDocPropertyOptions(); this.elPlaceHolder = tableAliasIntern(descriptor, deploy.getElPlaceHolder(), false, null); this.elPlaceHolderEncrypted = tableAliasIntern(descriptor, deploy.getElPlaceHolder(), dbEncrypted, dbColumn); this.elPrefix = deploy.getElPrefix(); this.softDelete = deploy.isSoftDelete(); if (softDelete) { ScalarTypeBoolean.BooleanBase boolType = (ScalarTypeBoolean.BooleanBase) scalarType; this.softDeleteDbSet = dbColumn + "=" + boolType.getDbTrueLiteral(); this.softDeleteDbPredicate = "." + dbColumn + "," + boolType.getDbFalseLiteral() + ")=" + boolType.getDbFalseLiteral(); } else { this.softDeleteDbSet = null; this.softDeleteDbPredicate = null; } this.jsonSerialize = deploy.isJsonSerialize(); this.jsonDeserialize = deploy.isJsonDeserialize(); } private String tableAliasIntern(BeanDescriptor> descriptor, String s, boolean dbEncrypted, String dbColumn) { if (descriptor != null) { s = StringHelper.replaceString(s, "${ta}.", "${}"); s = StringHelper.replaceString(s, "${ta}", "${}"); if (dbEncrypted) { s = dbEncryptFunction.getDecryptSql(s); String namedParam = ":encryptkey_" + descriptor.getBaseTable() + "___" + dbColumn; s = StringHelper.replaceString(s, "?", namedParam); } } return InternString.intern(s); } /** * Create a Matching BeanProperty with some attributes overridden. *
* Primarily for supporting Embedded beans with overridden dbColumn * mappings. *
*/ public BeanProperty(BeanProperty source, BeanPropertyOverride override) { this.descriptor = source.descriptor; this.name = InternString.intern(source.getName()); this.propertyIndex = source.propertyIndex; this.dbColumn = InternString.intern(override.getDbColumn()); // override with sqlFormula not currently supported this.sqlFormulaJoin = null; this.sqlFormulaSelect = null; this.formula = false; this.aggregation = null; this.excludedFromHistory = source.excludedFromHistory; this.draft = source.draft; this.draftDirty = source.draftDirty; this.draftOnly = source.draftOnly; this.draftReset = source.draftReset; this.softDelete = source.softDelete; this.softDeleteDbSet = source.softDeleteDbSet; this.softDeleteDbPredicate = source.softDeleteDbPredicate; this.fetchEager = source.fetchEager; this.unidirectionalShadow = source.unidirectionalShadow; this.discriminator = source.discriminator; this.localEncrypted = source.isLocalEncrypted(); this.isTransient = source.isTransient(); this.secondaryTable = source.isSecondaryTable(); this.secondaryTableJoin = source.secondaryTableJoin; this.secondaryTableJoinPrefix = source.secondaryTableJoinPrefix; this.dbComment = source.dbComment; this.dbBind = source.getDbBind(); this.dbEncrypted = source.isDbEncrypted(); this.dbEncryptedType = source.getDbEncryptedType(); this.dbEncryptFunction = source.dbEncryptFunction; this.dbRead = source.isDbRead(); this.dbInsertable = source.isDbInsertable(); this.dbUpdatable = source.isDbUpdatable(); this.nullable = source.isNullable(); this.unique = source.isUnique(); this.naturalKey = source.isNaturalKey(); this.dbLength = source.getDbLength(); this.dbScale = source.getDbScale(); this.dbColumnDefn = InternString.intern(source.getDbColumnDefn()); this.dbColumnDefault = source.dbColumnDefault; this.inherited = source.isInherited(); this.owningType = source.owningType; this.local = owningType.equals(descriptor.getBeanType()); this.version = source.isVersion(); this.embedded = source.isEmbedded(); this.id = source.isId(); this.generatedProperty = source.getGeneratedProperty(); this.getter = source.getter; this.setter = source.setter; this.dbType = source.getDbType(true); this.scalarType = source.scalarType; this.lob = isLobType(dbType); this.propertyType = source.getPropertyType(); this.field = source.getField(); this.docOptions = source.docOptions; this.elPrefix = override.replace(source.elPrefix, source.dbColumn); this.elPlaceHolder = override.replace(source.elPlaceHolder, source.dbColumn); this.elPlaceHolderEncrypted = override.replace(source.elPlaceHolderEncrypted, source.dbColumn); this.jsonSerialize = source.jsonSerialize; this.jsonDeserialize = source.jsonDeserialize; } /** * Initialise the property before returning to client code. Used to * initialise variables that can't be done in construction due to recursive * issues. */ public void initialise() { // do nothing for normal BeanProperty if (!isTransient && scalarType == null) { String msg = "No ScalarType assigned to " + descriptor.getFullName() + "." + getName(); throw new RuntimeException(msg); } } /** * Return the order this property appears in the bean. */ public int getDeployOrder() { return deployOrder; } /** * Set the order this property appears in the bean. */ public void setDeployOrder(int deployOrder) { this.deployOrder = deployOrder; } public ElPropertyValue buildElPropertyValue(String propName, String remainder, ElPropertyChainBuilder chain, boolean propertyDeploy) { return null; } /** * Return the BeanDescriptor that owns this property. */ public BeanDescriptor> getBeanDescriptor() { return descriptor; } /** * Return true is this is a simple scalar property. */ public boolean isScalar() { return true; } /** * Return true if this property should have a DB Column created in DDL. */ public boolean isDDLColumn() { return !formula && !secondaryTable && (aggregation == null); } /** * Return true if this property is based on a formula. */ public boolean isFormula() { return formula; } /** * Return true if this property maps to the inheritance discriminator column. */ public boolean isDiscriminator() { return discriminator; } /** * Return true if the underlying type is mutable. */ public boolean isMutableScalarType() { return scalarType != null && scalarType.isMutable(); } /** * Return the encrypt key for the column matching this property. */ public EncryptKey getEncryptKey() { return descriptor.getEncryptKey(this); } public String getDecryptProperty(String propertyName) { return dbEncryptFunction.getDecryptSql(propertyName); } public String getDecryptSql() { return dbEncryptFunction.getDecryptSql(this.getDbColumn()); } public String getDecryptSql(String tableAlias) { return dbEncryptFunction.getDecryptSql(tableAlias + "." + this.getDbColumn()); } /** * Add any extra joins required to support this property. Generally a no * operation except for a OneToOne exported. */ public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) { if (formula && sqlFormulaJoin != null) { ctx.appendFormulaJoin(sqlFormulaJoin, joinType); } else if (secondaryTableJoin != null) { String relativePrefix = ctx.getRelativePrefix(secondaryTableJoinPrefix); secondaryTableJoin.addJoin(joinType, relativePrefix, ctx); } } /** * Returns null unless this property is using a secondary table. In that * case this returns the logical property prefix. */ public String getSecondaryTableJoinPrefix() { return secondaryTableJoinPrefix; } public boolean isAggregation() { return aggregation != null; } public void appendSelect(DbSqlContext ctx, boolean subQuery) { if (aggregation != null) { ctx.appendRawColumn(aggregation); } else if (formula) { ctx.appendFormulaSelect(sqlFormulaSelect); } else if (!isTransient && !ignoreDraftOnlyProperty(ctx.isDraftQuery())) { if (secondaryTableJoin != null) { String relativePrefix = ctx.getRelativePrefix(secondaryTableJoinPrefix); ctx.pushTableAlias(relativePrefix); } if (dbEncrypted) { String decryptSql = getDecryptSql(ctx.peekTableAlias()); ctx.appendRawColumn(decryptSql); ctx.addEncryptedProp(this); } else { ctx.appendColumn(dbColumn); } if (secondaryTableJoin != null) { ctx.popTableAlias(); } } } @Override public boolean isMany() { return false; } public boolean isAssignableFrom(Class> type) { return owningType.isAssignableFrom(type); } public void loadIgnore(DbReadContext ctx) { scalarType.loadIgnore(ctx.getDataReader()); } public void load(SqlBeanLoad sqlBeanLoad) { sqlBeanLoad.load(this); } public void buildRawSqlSelectChain(String prefix, List* This uses format() where possible to store the value as a string and this * is done to make any resulting Java object serialisation content smaller as * strings get special treatment. *
*/ public Object getCacheDataValue(EntityBean bean) { Object value = getValue(bean); if (value == null || scalarType.isBinaryType()) { return value; } else { // convert to string as an optimisation for java object serialisation return scalarType.format(value); } } /** * Read the value for this property from L2 cache entry and set it to the bean. ** This uses parse() as per the comment in getCacheDataValue(). *
*/ public void setCacheDataValue(EntityBean bean, Object cacheData, PersistenceContext context) { if (cacheData instanceof String) { // parse back from string to support optimisation of java object serialisation cacheData = scalarType.parse((String) cacheData); } setValue(bean, cacheData); } /** * Get the property value from a compound value type. */ public Object getValueObject(Object bean) { throw new RuntimeException("Expected to be called only on BeanPropertyCompoundScalar"); } public Object getVal(Object bean) { return getValue((EntityBean) bean); } /** * Return the value of the property method. */ public Object getValue(EntityBean bean) { try { return getter.get(bean); } catch (Exception ex) { String beanType = bean == null ? "null" : bean.getClass().getName(); String msg = "get " + name + " on [" + descriptor + "] type[" + beanType + "] threw error."; throw new RuntimeException(msg, ex); } } public Object getValueIntercept(EntityBean bean) { try { return getter.getIntercept(bean); } catch (Exception ex) { String beanType = bean == null ? "null" : bean.getClass().getName(); String msg = "getIntercept " + name + " on [" + descriptor + "] type[" + beanType + "] threw error."; throw new RuntimeException(msg, ex); } } public Object convert(Object value) { if (value == null) { return null; } return convertToLogicalType(value); } @Override public void pathSet(Object bean, Object value) { if (bean != null) { Object logicalVal = convertToLogicalType(value); setValueIntercept((EntityBean) bean, logicalVal); } } @Override public Object pathGet(Object bean) { if (bean == null) { return null; } return getValueIntercept((EntityBean) bean); } @Override public Object pathGetNested(Object bean) { throw new RuntimeException("Not expected to call this"); } /** * Return the name of the property. */ public String getName() { return name; } /** * Return the position of this property in the enhanced bean. */ public int getPropertyIndex() { return propertyIndex; } public String getElName() { return name; } @Override public boolean containsFormulaWithJoin() { return formula && sqlFormulaJoin != null; } public boolean containsManySince(String sinceProperty) { return containsMany(); } public boolean containsMany() { return aggregation != null; } @Override public String getAssocIsEmpty(SpiExpressionRequest request, String path) { // overridden in BanePropertyAssocMany throw new RuntimeException("Not Supported or Expected"); } public Object[] getAssocIdValues(EntityBean bean) { // Returns null as not an AssocOne. return null; } public String getAssocIdExpression(String prefix, String operator) { // Returns null as not an AssocOne. return null; } public String getAssocIdInExpr(String prefix) { // Returns null as not an AssocOne. return null; } public String getAssocIdInValueExpr(int size) { // Returns null as not an AssocOne. return null; } @Override public boolean isAssocMany() { // Returns false - override in BeanPropertyAssocMany. return false; } public boolean isAssocId() { // Returns false - override in BeanPropertyAssocOne. return false; } public boolean isAssocProperty() { // Returns false - override in BeanPropertyAssocOne. return false; } public String getElPlaceholder(boolean encrypted) { return encrypted ? elPlaceHolderEncrypted : elPlaceHolder; } public String getElPrefix() { return elPrefix; } /** * Return the full name of this property. */ public String getFullBeanName() { return descriptor.getFullName() + "." + name; } /** * Return true if the mutable value is considered dirty. * This is only used for 'mutable' scalar types like hstore etc. */ public boolean isDirtyValue(Object value) { return scalarType.isDirty(value); } /** * Return the scalarType. */ @SuppressWarnings(value = "unchecked") public ScalarType