package com.avaje.ebeaninternal.server.deploy; import java.sql.SQLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.persistence.PersistenceException; import com.avaje.ebean.EbeanServer; import com.avaje.ebean.Expression; import com.avaje.ebean.InvalidValue; import com.avaje.ebean.Query; import com.avaje.ebean.SqlUpdate; import com.avaje.ebean.Transaction; import com.avaje.ebean.bean.BeanCollection; import com.avaje.ebean.bean.BeanCollection.ModifyListenMode; import com.avaje.ebean.bean.BeanCollectionAdd; import com.avaje.ebean.bean.BeanCollectionLoader; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.core.DefaultSqlUpdate; import com.avaje.ebeaninternal.server.deploy.id.ImportedId; import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany; 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.query.SqlBeanLoad; import com.avaje.ebeaninternal.server.text.json.ReadJsonContext; import com.avaje.ebeaninternal.server.text.json.ReadJsonContext.ReadBeanState; import com.avaje.ebeaninternal.server.text.json.WriteJsonContext; /** * Property mapped to a List Set or Map. */ public class BeanPropertyAssocMany extends BeanPropertyAssoc { /** * Join for manyToMany intersection table. */ final TableJoin intersectionJoin; /** * For ManyToMany this is the Inverse join used to build reference queries. */ final TableJoin inverseJoin; /** * Flag to indicate that this is a unidirectional relationship. */ final boolean unidirectional; /** * Flag to indicate manyToMany relationship. */ final boolean manyToMany; final String fetchOrderBy; final String mapKey; /** * The type of the many, set, list or map. */ final ManyType manyType; final String serverName; final ModifyListenMode modifyListenMode; BeanProperty mapKeyProperty; /** * Derived list of exported property and matching foreignKey */ ExportedProperty[] exportedProperties; /** * Property on the 'child' bean that links back to the 'master'. */ BeanPropertyAssocOne childMasterProperty; boolean embeddedExportedProperties; BeanCollectionHelp help; ImportedId importedId; String deleteByParentIdSql; String deleteByParentIdInSql; /** * Create this property. */ public BeanPropertyAssocMany(BeanDescriptorMap owner, BeanDescriptor descriptor, DeployBeanPropertyAssocMany deploy) { super(owner, descriptor, deploy); this.unidirectional = deploy.isUnidirectional(); this.manyToMany = deploy.isManyToMany(); this.serverName = descriptor.getServerName(); this.manyType = deploy.getManyType(); this.mapKey = deploy.getMapKey(); this.fetchOrderBy = deploy.getFetchOrderBy(); this.intersectionJoin = deploy.createIntersectionTableJoin(); this.inverseJoin = deploy.createInverseTableJoin(); this.modifyListenMode = deploy.getModifyListenMode(); } public void initialise() { super.initialise(); if (!isTransient){ this.help = BeanCollectionHelpFactory.create(this); if (manyToMany){ // only manyToMany's have imported properties importedId = createImportedId(this, targetDescriptor, tableJoin); } else { // find the property in the many that matches // back to the master (Order in the OrderDetail bean) childMasterProperty = initChildMasterProperty(); if (childMasterProperty != null){ childMasterProperty.setRelationshipProperty(this); } } if (mapKey != null){ mapKeyProperty = initMapKeyProperty(); } exportedProperties = createExported(); if (exportedProperties.length > 0){ embeddedExportedProperties = exportedProperties[0].isEmbedded(); } String delStmt; if (manyToMany){ delStmt = "delete from "+inverseJoin.getTable()+" where "; } else { delStmt = "delete from "+targetDescriptor.getBaseTable()+" where "; } deleteByParentIdSql = delStmt + deriveWhereParentIdSql(false); deleteByParentIdInSql = delStmt + deriveWhereParentIdSql(true); } } @Override public Object getValue(Object bean) { return super.getValue(bean); } @Override public Object getValueIntercept(Object bean) { return super.getValueIntercept(bean); } @Override public void setValue(Object bean, Object value) { super.setValue(bean, value); } @Override public void setValueIntercept(Object bean, Object value) { super.setValueIntercept(bean, value); } public ElPropertyValue buildElPropertyValue(String propName, String remainder, ElPropertyChainBuilder chain, boolean propertyDeploy) { return createElPropertyValue(propName, remainder, chain, propertyDeploy); } public SqlUpdate deleteByParentId(Object parentId, List parentIdist) { if (parentId != null){ return deleteByParentId(parentId); } else { return deleteByParentIdList(parentIdist); } } private SqlUpdate deleteByParentId(Object parentId) { DefaultSqlUpdate sqlDelete = new DefaultSqlUpdate(deleteByParentIdSql); bindWhereParendId(sqlDelete, parentId); return sqlDelete; } /** * Find the Id's of detail beans given a parent Id or list of parent Id's. */ public List findIdsByParentId(Object parentId, List parentIdist, Transaction t, ArrayList excludeDetailIds) { if (parentId != null){ return findIdsByParentId(parentId, t, excludeDetailIds); } else { return findIdsByParentIdList(parentIdist, t, excludeDetailIds); } } private List findIdsByParentId(Object parentId, Transaction t, ArrayList excludeDetailIds) { String rawWhere = deriveWhereParentIdSql(false); EbeanServer server = getBeanDescriptor().getEbeanServer(); Query q = server.find(getPropertyType()) .where().raw(rawWhere).query(); bindWhereParendId(1, q, parentId); if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) { Expression idIn = q.getExpressionFactory().idIn(excludeDetailIds); q.where().not(idIn); } return server.findIds(q, t); } private List findIdsByParentIdList(List parentIdist, Transaction t, ArrayList excludeDetailIds) { String rawWhere = deriveWhereParentIdSql(true); String inClause = targetIdBinder.getIdInValueExpr(parentIdist.size()); String expr = rawWhere+inClause; EbeanServer server = getBeanDescriptor().getEbeanServer(); Query q = server.find(getPropertyType()) .where().raw(expr).query(); int pos = 1; for (int i = 0; i < parentIdist.size(); i++) { pos = bindWhereParendId(pos, q, parentIdist.get(i)); } if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) { Expression idIn = q.getExpressionFactory().idIn(excludeDetailIds); q.where().not(idIn); } return server.findIds(q, t); } private SqlUpdate deleteByParentIdList(List parentIdist) { StringBuilder sb = new StringBuilder(100); sb.append(deleteByParentIdInSql); String inClause = targetIdBinder.getIdInValueExpr(parentIdist.size()); sb.append(inClause); DefaultSqlUpdate delete = new DefaultSqlUpdate(sb.toString()); for (int i = 0; i < parentIdist.size(); i++) { bindWhereParendId(delete, parentIdist.get(i)); } return delete; } /** * Set the lazy load server to help create reference collections (that lazy * load on demand). */ public void setLoader(BeanCollectionLoader loader){ if (help != null){ help.setLoader(loader); } } /** * Return the mode for listening to modifications to collections for this * association. */ public ModifyListenMode getModifyListenMode() { return modifyListenMode; } /** * Ignore changes for Many properties. */ public boolean hasChanged(Object bean, Object oldValues) { return false; } @Override public void appendSelect(DbSqlContext ctx, boolean subQuery) { } @Override public void loadIgnore(DbReadContext ctx) { // nothing to ignore for Many } @Override public void load(SqlBeanLoad sqlBeanLoad) throws SQLException { sqlBeanLoad.loadAssocMany(this); } @Override public Object readSet(DbReadContext ctx, Object bean, Class type) throws SQLException { return null; } @Override public Object read(DbReadContext ctx) throws SQLException { return null; } @Override public boolean isValueLoaded(Object value) { if (value instanceof BeanCollection){ return ((BeanCollection)value).isPopulated(); } return true; } public void add(BeanCollection collection, Object bean) { help.add(collection, bean); } @Override public InvalidValue validateCascade(Object manyValue) { ArrayList errs = help.validate(manyValue); if (errs == null){ return null; } else { return new InvalidValue("recurse.many", targetDescriptor.getFullName(), manyValue, InvalidValue.toArray(errs)); } } /** * Refresh the appropriate list set or map. */ public void refresh(EbeanServer server, Query query, Transaction t, Object parentBean) { help.refresh(server, query, t, parentBean); } /** * Apply the refreshed BeanCollection to the property of the parentBean. */ public void refresh(BeanCollection bc, Object parentBean) { help.refresh(bc, parentBean); } /** * Return the Id values from the given bean. */ @Override public Object[] getAssocOneIdValues(Object bean) { return targetDescriptor.getIdBinder().getIdValues(bean); } /** * Return the Id expression to add to where clause etc. */ public String getAssocOneIdExpr(String prefix, String operator) { return targetDescriptor.getIdBinder().getAssocOneIdExpr(prefix, operator); } /** * Return the logical id value expression taking into account embedded id's. */ @Override public String getAssocIdInValueExpr(int size){ return targetDescriptor.getIdBinder().getIdInValueExpr(size); } /** * Return the logical id in expression taking into account embedded id's. */ @Override public String getAssocIdInExpr(String prefix){ return targetDescriptor.getIdBinder().getAssocIdInExpr(prefix); } @Override public boolean isAssocId() { return true; } @Override public boolean isAssocProperty() { return true; } /** * Returns true. */ @Override public boolean containsMany(){ return true; } /** * Return the many type. */ public ManyType getManyType() { return manyType; } /** * Return true if this is many to many. */ public boolean isManyToMany() { return manyToMany; } /** * ManyToMany only, join from local table to intersection table. */ public TableJoin getIntersectionTableJoin() { return intersectionJoin; } /** * Set the join properties from the parent bean to the child bean. * This is only valid for OneToMany and NOT valid for ManyToMany. */ public void setJoinValuesToChild(Object parent, Object child, Object mapKeyValue) { if (mapKeyProperty != null){ mapKeyProperty.setValue(child, mapKeyValue); } if (!manyToMany){ if (childMasterProperty != null){ // bidirectional in the sense that the 'master' property // exists on the 'detail' bean childMasterProperty.setValue(child, parent); } else { // unidirectional in the sense that the 'master' property // does NOT exist on the 'detail' bean } } } /** * Return the order by clause used to order the fetching of the data for * this list, set or map. */ public String getFetchOrderBy() { return fetchOrderBy; } /** * Return the default mapKey when returning a Map. */ public String getMapKey() { return mapKey; } public BeanCollection createReferenceIfNull(Object parentBean) { Object v = getValue(parentBean); if (v instanceof BeanCollection){ BeanCollection bc = (BeanCollection)v; return bc.isReference() ? bc : null; } else { return createReference(parentBean); } } public BeanCollection createReference(Object parentBean) { BeanCollection ref = help.createReference(parentBean, name); setValue(parentBean, ref); return ref; } public Object createEmpty(boolean vanilla) { return help.createEmpty(vanilla); } public BeanCollectionAdd getBeanCollectionAdd(Object bc, String mapKey) { return help.getBeanCollectionAdd(bc, mapKey); } public Object getParentId(Object parentBean) { return descriptor.getId(parentBean); } private void bindWhereParendId(DefaultSqlUpdate sqlUpd, Object parentId){ if (exportedProperties.length == 1){ sqlUpd.addParameter(parentId); return; } for (int i = 0; i < exportedProperties.length; i++) { Object embVal = exportedProperties[i].getValue(parentId); sqlUpd.addParameter(embVal); } } private int bindWhereParendId(int pos, Query q, Object parentId) { if (exportedProperties.length == 1) { q.setParameter(pos++, parentId); } else { for (int i = 0; i < exportedProperties.length; i++) { Object embVal = exportedProperties[i].getValue(parentId); q.setParameter(pos++, embVal); } } return pos; } private String deriveWhereParentIdSql(boolean inClause) { StringBuilder sb = new StringBuilder(); if (inClause){ sb.append("("); } for (int i = 0; i < exportedProperties.length; i++) { String fkColumn = exportedProperties[i].getForeignDbColumn(); if (i > 0){ String s = inClause ? "," : " and "; sb.append(s); } sb.append(fkColumn); if (!inClause){ sb.append("=? "); } } if (inClause){ sb.append(")"); } return sb.toString(); } public void setPredicates(SpiQuery query, Object parentBean) { if (manyToMany){ // for ManyToMany lazy loading we need to include a // join to the intersection table. The predicate column // is not on the 'destination many table'. query.setIncludeTableJoin(inverseJoin); } if (embeddedExportedProperties) { // use the EmbeddedId object instead of the parentBean BeanProperty[] uids = descriptor.propertiesId(); parentBean = uids[0].getValue(parentBean); } for (int i = 0; i < exportedProperties.length; i++) { Object val = exportedProperties[i].getValue(parentBean); String fkColumn = exportedProperties[i].getForeignDbColumn(); if (!manyToMany){ fkColumn = targetDescriptor.getBaseTableAlias()+"."+fkColumn; } else { // use hard coded alias for intersection table fkColumn = "int_."+fkColumn; } query.where().eq(fkColumn, val); } if (extraWhere != null){ // replace the table alias place holder String ta = targetDescriptor.getBaseTableAlias(); String where = StringHelper.replaceString(extraWhere, "${ta}", ta); query.where().raw(where); } if (fetchOrderBy != null){ query.order(fetchOrderBy); } } /** * Create the array of ExportedProperty used to build reference objects. */ private ExportedProperty[] createExported() { BeanProperty[] uids = descriptor.propertiesId(); ArrayList list = new ArrayList(); if (uids.length == 1 && uids[0].isEmbedded()) { BeanPropertyAssocOne one = (BeanPropertyAssocOne) uids[0]; BeanDescriptor targetDesc = one.getTargetDescriptor(); BeanProperty[] emIds = targetDesc.propertiesBaseScalar(); try { for (int i = 0; i < emIds.length; i++) { ExportedProperty expProp = findMatch(true, emIds[i]); list.add(expProp); } } catch (PersistenceException e){ // not found as individual scalar properties e.printStackTrace(); } } else { for (int i = 0; i < uids.length; i++) { ExportedProperty expProp = findMatch(false, uids[i]); list.add(expProp); } } return (ExportedProperty[]) list.toArray(new ExportedProperty[list.size()]); } /** * Find the matching foreignDbColumn for a given local property. */ private ExportedProperty findMatch(boolean embedded,BeanProperty prop) { String matchColumn = prop.getDbColumn(); String searchTable; TableJoinColumn[] columns; if (manyToMany){ // look for column going to intersection columns = intersectionJoin.columns(); searchTable = intersectionJoin.getTable(); } else { columns = tableJoin.columns(); searchTable = tableJoin.getTable(); } for (int i = 0; i < columns.length; i++) { String matchTo = columns[i].getLocalDbColumn(); if (matchColumn.equalsIgnoreCase(matchTo)) { String foreignCol = columns[i].getForeignDbColumn(); return new ExportedProperty(embedded, foreignCol, prop); } } String msg = "Error with the Join on ["+getFullBeanName() +"]. Could not find the matching foreign key for ["+matchColumn+"] in table["+searchTable+"]?" +" Perhaps using a @JoinColumn with the name/referencedColumnName attributes swapped?"; throw new PersistenceException(msg); } /** * Return the child property that links back to the master bean. *

* Note that childMasterProperty will be null if a field is used instead of * a ManyToOne bean association. *

*/ private BeanPropertyAssocOne initChildMasterProperty() { if (unidirectional){ return null; } // search for the property, to see if it exists Class beanType = descriptor.getBeanType(); BeanDescriptor targetDesc = getTargetDescriptor(); BeanPropertyAssocOne[] ones = targetDesc.propertiesOne(); for (int i = 0; i < ones.length; i++) { BeanPropertyAssocOne prop = ones[i]; if (mappedBy != null){ // match using mappedBy as property name if (mappedBy.equalsIgnoreCase(prop.getName())) { return prop; } } else { // assume only one property that matches parent object type if (prop.getTargetType().equals(beanType)) { // found it, stop search return prop; } } } String msg = "Can not find Master [" + beanType + "] in Child[" + targetDesc + "]"; throw new RuntimeException(msg); } /** * Search for and return the mapKey property. */ private BeanProperty initMapKeyProperty() { // search for the property BeanDescriptor targetDesc = getTargetDescriptor(); Iterator it = targetDesc.propertiesAll(); while (it.hasNext()){ BeanProperty prop = it.next(); if (mapKey.equalsIgnoreCase(prop.getName())) { return prop; } } String from = descriptor.getFullName(); String to = targetDesc.getFullName(); String msg = from+": Could not find mapKey property ["+mapKey+"] on ["+to+"]"; throw new PersistenceException(msg); } public IntersectionRow buildManyDeleteChildren(Object parentBean, ArrayList excludeDetailIds) { IntersectionRow row = new IntersectionRow(tableJoin.getTable()); if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) { row.setExcludeIds(excludeDetailIds, getTargetDescriptor()); } buildExport(row, parentBean); return row; } public IntersectionRow buildManyToManyDeleteChildren(Object parentBean) { IntersectionRow row = new IntersectionRow(intersectionJoin.getTable()); buildExport(row, parentBean); return row; } public IntersectionRow buildManyToManyMapBean(Object parent, Object other) { IntersectionRow row = new IntersectionRow(intersectionJoin.getTable()); buildExport(row, parent); buildImport(row, other); return row; } private void buildExport(IntersectionRow row, Object parentBean) { if (embeddedExportedProperties) { BeanProperty[] uids = descriptor.propertiesId(); parentBean = uids[0].getValue(parentBean); } for (int i = 0; i < exportedProperties.length; i++) { Object val = exportedProperties[i].getValue(parentBean); String fkColumn = exportedProperties[i].getForeignDbColumn(); row.put(fkColumn, val); } } /** * Set the predicates for lazy loading of the association. * Handles predicates for both OneToMany and ManyToMany. */ private void buildImport(IntersectionRow row, Object otherBean) { importedId.buildImport(row, otherBean); } /** * Return true if the otherBean has an Id value. */ public boolean hasImportedId(Object otherBean) { return null != targetDescriptor.getId(otherBean); } public void jsonWrite(WriteJsonContext ctx, Object bean) { if(!this.jsonSerialize){ return; } Boolean include = ctx.includeMany(name); if (Boolean.FALSE.equals(include)){ return; } Object value = getValueIntercept(bean); if (value != null){ ctx.pushParentBeanMany(bean); help.jsonWrite(ctx, name, value, include != null); ctx.popParentBeanMany(); } } public void jsonRead(ReadJsonContext ctx, Object bean){ if(!this.jsonDeserialize){ return; } if (!ctx.readArrayBegin()) { // the array is null return; } Object collection = help.createEmpty(false); BeanCollectionAdd add = getBeanCollectionAdd(collection, null); do { ReadBeanState detailBeanState = targetDescriptor.jsonRead(ctx, name); if (detailBeanState == null){ // probably empty array break; } Object detailBean = detailBeanState.getBean(); add.addBean(detailBean); if (bean != null && childMasterProperty != null){ // bind detail bean back to master via mappedBy property childMasterProperty.setValue(detailBean, bean); detailBeanState.setLoaded(childMasterProperty.getName()); } detailBeanState.setLoadedState(); if (!ctx.readArrayNext()){ break; } } while(true); setValue(bean, collection); } }