package io.ebeaninternal.server.deploy;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.ebean.SqlUpdate;
import io.ebean.Transaction;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.BeanCollection.ModifyListenMode;
import io.ebean.bean.BeanCollectionAdd;
import io.ebean.bean.EntityBean;
import io.ebean.bean.PersistenceContext;
import io.ebean.plugin.PropertyAssocMany;
import io.ebean.text.PathProperties;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiExpressionRequest;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.api.SpiSqlUpdate;
import io.ebeaninternal.api.json.SpiJsonReader;
import io.ebeaninternal.api.json.SpiJsonWriter;
import io.ebeaninternal.server.deploy.id.ImportedId;
import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany;
import io.ebeaninternal.server.el.ElPropertyChainBuilder;
import io.ebeaninternal.server.el.ElPropertyValue;
import io.ebeaninternal.server.query.STreePropertyAssocMany;
import io.ebeaninternal.server.query.SqlBeanLoad;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.PersistenceException;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

Property mapped to a List Set or Map.
/** * Property mapped to a List Set or Map. */
public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements STreePropertyAssocMany, PropertyAssocMany { private static final Logger logger = LoggerFactory.getLogger(BeanPropertyAssocMany.class); private final BeanPropertyAssocManyJsonHelp jsonHelp;
Join for manyToMany intersection table.
/** * Join for manyToMany intersection table. */
private final TableJoin intersectionJoin; private final String intersectionPublishTable; private final String intersectionDraftTable; private final boolean orphanRemoval; private IntersectionTable intersectionTable;
For ManyToMany this is the Inverse join used to build reference queries.
/** * For ManyToMany this is the Inverse join used to build reference queries. */
final TableJoin inverseJoin;
Flag to indicate that this is a unidirectional relationship.
/** * Flag to indicate that this is a unidirectional relationship. */
private final boolean unidirectional; private final boolean o2mJoinTable;
Flag to indicate that the target has a order column to auto populate.
/** * Flag to indicate that the target has a order column to auto populate. */
private final boolean hasOrderColumn;
Flag to indicate manyToMany relationship.
/** * Flag to indicate manyToMany relationship. */
private final boolean manyToMany; private final boolean elementCollection;
Descriptor for the 'target' when the property maps to an element collection.
/** * Descriptor for the 'target' when the property maps to an element collection. */
final BeanDescriptor<T> elementDescriptor;
Order by used when fetch joining the associated many.
/** * Order by used when fetch joining the associated many. */
private final String fetchOrderBy;
Order by used when lazy loading the associated many.
/** * Order by used when lazy loading the associated many. */
private String lazyFetchOrderBy; private final String mapKey;
The type of the many, set, list or map.
/** * The type of the many, set, list or map. */
private final ManyType manyType; private final ModifyListenMode modifyListenMode; private BeanProperty mapKeyProperty;
Property on the 'child' bean that links back to the 'master'.
/** * Property on the 'child' bean that links back to the 'master'. */
private BeanPropertyAssocOne<?> childMasterProperty; private String childMasterIdProperty; private boolean embeddedExportedProperties; private BeanCollectionHelp<T> help; private ImportedId importedId; private BeanPropertyAssocManySqlHelp<T> sqlHelp;
Create this property.
/** * Create this property. */
public BeanPropertyAssocMany(BeanDescriptor<?> descriptor, DeployBeanPropertyAssocMany<T> deploy) { super(descriptor, deploy); this.unidirectional = deploy.isUnidirectional(); this.orphanRemoval = deploy.isOrphanRemoval(); this.o2mJoinTable = deploy.isO2mJoinTable(); this.hasOrderColumn = deploy.hasOrderColumn(); this.manyToMany = deploy.isManyToMany(); this.elementCollection = deploy.isElementCollection(); this.elementDescriptor = deploy.getElementDescriptor(); this.manyType = deploy.getManyType(); this.mapKey = deploy.getMapKey(); this.fetchOrderBy = deploy.getFetchOrderBy(); this.intersectionJoin = deploy.createIntersectionTableJoin(); if (intersectionJoin != null) { this.intersectionPublishTable = intersectionJoin.getTable(); this.intersectionDraftTable = deploy.getIntersectionDraftTable(); } else { this.intersectionPublishTable = null; this.intersectionDraftTable = null; } this.inverseJoin = deploy.createInverseTableJoin(); this.modifyListenMode = deploy.getModifyListenMode(); this.jsonHelp = descriptor.isJacksonCorePresent() ? new BeanPropertyAssocManyJsonHelp(this) : null; } @Override public void initialise(BeanDescriptorInitContext initContext) { super.initialise(initContext); initialiseAssocMany(); if (elementCollection) { // initialise all non-id properties (we don't have an Id property) elementDescriptor.initialiseOther(initContext); } } @Override void initialiseTargetDescriptor(BeanDescriptorInitContext initContext) { if (elementCollection) { targetDescriptor = elementDescriptor; } else { super.initialiseTargetDescriptor(initContext); } } private void initialiseAssocMany() { if (!isTransient) { this.help = BeanCollectionHelpFactory.create(this); if (hasJoinTable() || elementCollection) { 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(); this.sqlHelp = new BeanPropertyAssocManySqlHelp<>(this, exportedProperties); if (exportedProperties.length > 0) { embeddedExportedProperties = exportedProperties[0].isEmbedded(); if (fetchOrderBy != null) { lazyFetchOrderBy = sqlHelp.lazyFetchOrderBy(fetchOrderBy); } } } } String targetTable() { return beanTable.getBaseTable(); }
Initialise after the target bean descriptors have been all set.
/** * Initialise after the target bean descriptors have been all set. */
void initialisePostTarget() { if (childMasterProperty != null) { BeanProperty masterId = childMasterProperty.getTargetDescriptor().getIdProperty(); if (masterId != null) { // in docstore only, the master-id may be not available childMasterIdProperty = childMasterProperty.getName() + "." + masterId.getName(); } } } @Override public BeanPropertyAssocMany<?> asMany() { return this; } @Override public boolean isManyToManyWithHistory() { return manyToMany && !excludedFromHistory && descriptor.isHistorySupport(); } @Override protected void docStoreIncludeByDefault(PathProperties pathProps) { // by default not including "Many" properties in document store } @Override public void registerColumn(BeanDescriptor<?> desc, String prefix) { if (targetDescriptor != null) { desc.registerTable(targetDescriptor.getBaseTable(), this); } }
Return the underlying collection of beans.
/** * Return the underlying collection of beans. */
public Collection getRawCollection(EntityBean bean) { return help.underlying(getVal(bean)); }
Copy collection value if existing is empty.
/** * Copy collection value if existing is empty. */
@Override public void merge(EntityBean bean, EntityBean existing) { Object existingCollection = getVal(existing); if (existingCollection instanceof BeanCollection<?>) { BeanCollection<?> toBC = (BeanCollection<?>) existingCollection; if (!toBC.isPopulated()) { Object fromCollection = getVal(bean); if (fromCollection instanceof BeanCollection<?>) { BeanCollection<?> fromBC = (BeanCollection<?>) fromCollection; if (fromBC.isPopulated()) { toBC.loadFrom(fromBC); } } } } }
Add the bean to the appropriate collection on the parent bean.
/** * Add the bean to the appropriate collection on the parent bean. */
@Override public void addBeanToCollectionWithCreate(EntityBean parentBean, EntityBean detailBean, boolean withCheck) { BeanCollection<?> bc = (BeanCollection<?>) super.getValue(parentBean); if (bc == null) { bc = help.createEmpty(parentBean); setValue(parentBean, bc); } help.add(bc, detailBean, withCheck); }
Return true if this is considered 'empty' from a save perspective.
/** * Return true if this is considered 'empty' from a save perspective. */
public boolean isSkipSaveBeanCollection(EntityBean bean, boolean insertedParent) { Object val = getValue(bean); if (val == null) { return true; } if ((val instanceof BeanCollection<?>)) { return ((BeanCollection<?>) val).isSkipSave(); } if (insertedParent) { // check 'vanilla' collection types if (val instanceof Collection<?>) { return ((Collection<?>) val).isEmpty(); } if (val instanceof Map<?, ?>) { return ((Map<?, ?>) val).isEmpty(); } } return false; }
Reset the many properties to be empty and ready for reloading.

Used in bean refresh.

/** * Reset the many properties to be empty and ready for reloading. * <p> * Used in bean refresh. */
public void resetMany(EntityBean bean) { Object value = getValue(bean); if (value instanceof BeanCollection) { // reset the collection back to empty ((BeanCollection<?>) value).reset(bean, name); } else { createReference(bean); } } @Override public ElPropertyValue buildElPropertyValue(String propName, String remainder, ElPropertyChainBuilder chain, boolean propertyDeploy) { return createElPropertyValue(propName, remainder, chain, propertyDeploy); } @Override public void buildRawSqlSelectChain(String prefix, List<String> selectChain) { // do not add to the selectChain at the top level of the Many bean } public SpiSqlUpdate deleteByParentId(Object parentId, List<Object> parentIdist) { if (parentId != null) { return sqlHelp.deleteByParentId(parentId); } else { return sqlHelp.deleteByParentIdList(parentIdist); } }
Find the Id's of detail beans given a parent Id or list of parent Id's.
/** * Find the Id's of detail beans given a parent Id or list of parent Id's. */
public List<Object> findIdsByParentId(Object parentId, List<Object> parentIdList, Transaction t, List<Object> excludeDetailIds, boolean hard) { if (parentId != null) { return sqlHelp.findIdsByParentId(parentId, t, excludeDetailIds, hard); } else { return sqlHelp.findIdsByParentIdList(parentIdList, t, excludeDetailIds, hard); } }
Exclude many properties from bean cache data.
/** * Exclude many properties from bean cache data. */
@Override public boolean isCacheDataInclude() { // this would change for DB Array type support return false; }
Add the loaded current bean to its associated parent.

Helper method used by Elastic integration when loading with a persistence context.

/** * Add the loaded current bean to its associated parent. * <p> * Helper method used by Elastic integration when loading with a persistence context. */
@Override public void lazyLoadMany(EntityBean current) { EntityBean parentBean = childMasterProperty.getValueAsEntityBean(current); if (parentBean != null) { addBeanToCollectionWithCreate(parentBean, current, true); } } public void addWhereParentIdIn(SpiQuery<?> query, List<Object> parentIds, boolean useDocStore) { if (useDocStore) { // assumes the ManyToOne property is included query.where().in(childMasterIdProperty, parentIds); } else { sqlHelp.addWhereParentIdIn(query, parentIds); } }
Set the lazy load server to help create reference collections (that lazy load on demand).
/** * Set the lazy load server to help create reference collections (that lazy * load on demand). */
public void setEbeanServer(SpiEbeanServer server) { if (help != null) { help.setLoader(server); } if (manyToMany) { intersectionTable = initIntersectionTable(); } } private IntersectionTable initIntersectionTable() { IntersectionBuilder row = new IntersectionBuilder(intersectionPublishTable, intersectionDraftTable); for (ExportedProperty exportedProperty : exportedProperties) { row.addColumn(exportedProperty.getForeignDbColumn()); } importedId.buildImport(row); return row.build(); }
Return the intersection table helper.
/** * Return the intersection table helper. */
public IntersectionTable intersectionTable() { return intersectionTable; }
Return the mode for listening to modifications to collections for this association.
/** * Return the mode for listening to modifications to collections for this * association. */
public ModifyListenMode getModifyListenMode() { return modifyListenMode; } @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) { // do nothing, as a lazy loading BeanCollection 'reference' // is created and registered with the loading context // in SqlTreeNodeBean.createListProxies() } @Override public Object readSet(DbReadContext ctx, EntityBean bean) { return null; } @Override public Object read(DbReadContext ctx) { return null; } public void add(BeanCollection<?> collection, EntityBean bean) { help.add(collection, bean, false); } @Override public String getAssocIsEmpty(SpiExpressionRequest request, String path) { boolean softDelete = targetDescriptor.isSoftDelete(); StringBuilder sb = new StringBuilder(50); SpiQuery<?> query = request.getQueryRequest().getQuery(); if (hasJoinTable()) { sb.append(query.isAsDraft() ? intersectionDraftTable : intersectionPublishTable); } else { sb.append(targetDescriptor.getBaseTable(query.getTemporalMode())); } if (softDelete && hasJoinTable()) { sb.append(" x join "); sb.append(targetDescriptor.getBaseTable(query.getTemporalMode())); sb.append(" x2 on "); inverseJoin.addJoin("x2", "x", sb); } else { sb.append(" x"); } sb.append(" where "); for (int i = 0; i < exportedProperties.length; i++) { if (i > 0) { sb.append(" and "); } exportedProperties[i].appendWhere(sb, "x.", path); } if (softDelete) { String alias = hasJoinTable() ? "x2" : "x"; sb.append(" and ").append(targetDescriptor.getSoftDeletePredicate(alias)); } return sb.toString(); }
Return the Id values from the given bean.
/** * Return the Id values from the given bean. */
@Override public Object[] getAssocIdValues(EntityBean bean) { return targetDescriptor.getIdBinder().getIdValues(bean); }
Return the Id expression to add to where clause etc.
/** * Return the Id expression to add to where clause etc. */
@Override public String getAssocIdExpression(String prefix, String operator) { return targetDescriptor.getIdBinder().getAssocOneIdExpr(prefix, operator); }
Return the logical id value expression taking into account embedded id's.
/** * Return the logical id value expression taking into account embedded id's. */
@Override public String getAssocIdInValueExpr(boolean not, int size) { return targetDescriptor.getIdBinder().getIdInValueExpr(not, size); }
Return the logical id in expression taking into account embedded id's.
/** * 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 isMany() { return true; } public boolean hasOrderColumn() { return hasOrderColumn; } public boolean isOrphanRemoval() { return orphanRemoval; } @Override public boolean isAssocMany() { return true; } @Override public boolean isAssocId() { return true; } @Override public boolean isAssocProperty() { return true; }
Returns true.
/** * Returns true. */
@Override public boolean containsMany() { return true; }
Return the many type.
/** * Return the many type. */
public ManyType getManyType() { return manyType; }
Return true if this is many to many.
/** * Return true if this is many to many. */
@Override public boolean hasJoinTable() { return manyToMany || o2mJoinTable; }
Return true if this is a one to many with a join table.
/** * Return true if this is a one to many with a join table. */
public boolean isO2mJoinTable() { return o2mJoinTable; }
Return true if this is many to many.
/** * Return true if this is many to many. */
public boolean isManyToMany() { return manyToMany; } public boolean isElementCollection() { return elementCollection; }
Return the element bean descriptor (for an element collection only).
/** * Return the element bean descriptor (for an element collection only). */
public BeanDescriptor<T> getElementDescriptor() { return elementDescriptor; }
ManyToMany only, join from local table to intersection table.
/** * ManyToMany only, join from local table to intersection table. */
@Override 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.
/** * 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(EntityBean parent, EntityBean child, Object mapKeyValue) { if (mapKeyProperty != null) { mapKeyProperty.setValue(child, mapKeyValue); } if (!manyToMany && childMasterProperty != null) { // bidirectional in the sense that the 'master' property // exists on the 'detail' bean childMasterProperty.setValue(child, parent); } }
Return the order by clause used to order the fetching of the data for this list, set or map.
/** * 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 order by for use when lazy loading the associated collection.
/** * Return the order by for use when lazy loading the associated collection. */
public String getLazyFetchOrderBy() { return lazyFetchOrderBy; }
Return the default mapKey when returning a Map.
/** * Return the default mapKey when returning a Map. */
public String getMapKey() { return mapKey; } @Override public BeanCollection<?> createReference(EntityBean localBean, boolean forceNewReference) { return forceNewReference ? createReference(localBean) : createReferenceIfNull(localBean); } @Override public BeanCollection<?> createReferenceIfNull(EntityBean parentBean) { Object v = getValue(parentBean); if (v instanceof BeanCollection<?>) { BeanCollection<?> bc = (BeanCollection<?>) v; return bc.isReference() ? bc : null; } else if (v != null) { return null; } else { return createReference(parentBean); } } public BeanCollection<?> createReference(EntityBean parentBean) { BeanCollection<?> ref = help.createReference(parentBean); setValue(parentBean, ref); return ref; } public BeanCollection<T> createEmpty(EntityBean parentBean) { return help.createEmpty(parentBean); } private BeanCollectionAdd getBeanCollectionAdd(Object bc) { return help.getBeanCollectionAdd(bc, null); } public Object getParentId(EntityBean parentBean) { return descriptor.getId(parentBean); } @Override public void addSelectExported(DbSqlContext ctx, String tableAlias) { String alias = hasJoinTable() ? "int_" : tableAlias; if (alias == null) { alias = "t0"; } for (ExportedProperty exportedProperty : exportedProperties) { ctx.appendColumn(alias, exportedProperty.getForeignDbColumn()); } }
Create the array of ExportedProperty used to build reference objects.
/** * Create the array of ExportedProperty used to build reference objects. */
private ExportedProperty[] createExported() { BeanProperty idProp = descriptor.getIdProperty(); ArrayList<ExportedProperty> list = new ArrayList<>(); if (idProp != null && idProp.isEmbedded()) { BeanPropertyAssocOne<?> one = (BeanPropertyAssocOne<?>) idProp; try { for (BeanProperty emId : one.getTargetDescriptor().propertiesBaseScalar()) { list.add(findMatch(true, emId)); } } catch (PersistenceException e) { // not found as individual scalar properties logger.error("Could not find a exported property?", e); } } else { if (idProp != null) { list.add(findMatch(false, idProp)); } } return list.toArray(new ExportedProperty[0]); }
Find the matching foreignDbColumn for a given local property.
/** * Find the matching foreignDbColumn for a given local property. */
private ExportedProperty findMatch(boolean embedded, BeanProperty prop) { if (hasJoinTable()) { // look for column going to intersection return findMatch(embedded, prop, prop.getDbColumn(), intersectionJoin); } else { return findMatch(embedded, prop, prop.getDbColumn(), tableJoin); } }
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.

/** * Return the child property that links back to the master bean. * <p> * Note that childMasterProperty will be null if a field is used instead of * a ManyToOne bean association. * </p> */
private BeanPropertyAssocOne<?> initChildMasterProperty() { if (unidirectional) { return null; } // search for the property, to see if it exists Class<?> beanType = descriptor.getBeanType(); BeanDescriptor<?> targetDesc = getTargetDescriptor(); for (BeanPropertyAssocOne<?> prop : targetDesc.propertiesOne()) { 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; } } } throw new RuntimeException("Can not find Master [" + beanType + "] in Child[" + targetDesc + "]"); }
Search for and return the mapKey property.
/** * Search for and return the mapKey property. */
private BeanProperty initMapKeyProperty() { // search for the property BeanDescriptor<?> targetDesc = getTargetDescriptor(); for (BeanProperty prop : targetDesc.propertiesAll()) { if (mapKey.equalsIgnoreCase(prop.getName())) { return prop; } } String from = descriptor.getFullName(); String to = targetDesc.getFullName(); throw new PersistenceException(from + ": Could not find mapKey property [" + mapKey + "] on [" + to + "]"); } public IntersectionRow buildManyDeleteChildren(EntityBean parentBean, List<Object> excludeDetailIds) { IntersectionRow row = new IntersectionRow(tableJoin.getTable(), targetDescriptor); if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) { row.setExcludeIds(excludeDetailIds, getTargetDescriptor()); } buildExport(row, parentBean); return row; } public IntersectionRow buildManyToManyDeleteChildren(EntityBean parentBean, boolean publish) { String tableName = publish ? intersectionPublishTable : intersectionDraftTable; IntersectionRow row = new IntersectionRow(tableName); buildExport(row, parentBean); return row; } public IntersectionRow buildManyToManyMapBean(EntityBean parent, EntityBean other, boolean publish) { String tableName = publish ? intersectionPublishTable : intersectionDraftTable; IntersectionRow row = new IntersectionRow(tableName); buildExport(row, parent); buildImport(row, other); return row; }
Register the mapping of intersection table to associated draft table.
/** * Register the mapping of intersection table to associated draft table. */
void registerDraftIntersectionTable(BeanDescriptorInitContext initContext) { if (hasDraftIntersection()) { initContext.addDraftIntersection(intersectionPublishTable, intersectionDraftTable); } }
Return true if the relationship is a ManyToMany with the intersection having an associated draft table.
/** * Return true if the relationship is a ManyToMany with the intersection having an associated draft table. */
private boolean hasDraftIntersection() { return intersectionDraftTable != null && !intersectionDraftTable.equals(intersectionPublishTable); }
Bind the two side of a many to many to the given SqlUpdate.
/** * Bind the two side of a many to many to the given SqlUpdate. */
public void intersectionBind(SqlUpdate sql, EntityBean parentBean, EntityBean other) { if (embeddedExportedProperties) { BeanProperty idProp = descriptor.getIdProperty(); parentBean = (EntityBean) idProp.getValue(parentBean); } for (ExportedProperty exportedProperty : exportedProperties) { sql.setParameter(exportedProperty.getValue(parentBean)); } importedId.bindImport(sql, other); } private void buildExport(IntersectionRow row, EntityBean parentBean) { if (embeddedExportedProperties) { BeanProperty idProp = descriptor.getIdProperty(); parentBean = (EntityBean) idProp.getValue(parentBean); } for (ExportedProperty exportedProperty : exportedProperties) { Object val = exportedProperty.getValue(parentBean); String fkColumn = exportedProperty.getForeignDbColumn(); row.put(fkColumn, val); } }
Set the predicates for lazy loading of the association. Handles predicates for both OneToMany and ManyToMany.
/** * Set the predicates for lazy loading of the association. * Handles predicates for both OneToMany and ManyToMany. */
private void buildImport(IntersectionRow row, EntityBean otherBean) { importedId.buildImport(row, otherBean); }
Return true if the otherBean has an Id value.
/** * Return true if the otherBean has an Id value. */
public boolean hasImportedId(EntityBean otherBean) { return null != targetDescriptor.getId(otherBean); }
Skip JSON write value for ToMany property.
/** * Skip JSON write value for ToMany property. */
@Override public void jsonWriteValue(SpiJsonWriter writeJson, Object value) { // do nothing, exclude ToMany properties } @Override public void jsonWrite(SpiJsonWriter ctx, EntityBean bean) throws IOException { 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); if (help != null) { jsonWriteCollection(ctx, name, value, include != null); } else { if (isTransient && targetDescriptor == null) { ctx.writeValueUsingObjectMapper(name, value); } else { Collection<?> collection = (Collection<?>) value; if (!collection.isEmpty() || ctx.isIncludeEmpty()) { ctx.toJson(name, collection); } } } ctx.popParentBeanMany(); } } @Override public void jsonRead(SpiJsonReader readJson, EntityBean parentBean) throws IOException { jsonHelp.jsonRead(readJson, parentBean); } @SuppressWarnings("unchecked") void publishMany(EntityBean draft, EntityBean live) { // collections will not be null due to enhancement BeanCollection<T> draftVal = (BeanCollection<T>) getValueIntercept(draft); BeanCollection<T> liveVal = (BeanCollection<T>) getValueIntercept(live); // Organise the existing live beans into map keyed by id Map<Object, T> liveBeansAsMap = liveBeansAsMap(liveVal); // publish from each draft to live bean creating new live beans as required draftVal.size(); Collection<T> actualDetails = draftVal.getActualDetails(); for (T bean : actualDetails) { Object id = targetDescriptor.getId((EntityBean) bean); T liveBean = liveBeansAsMap.remove(id); if (isManyToMany()) { if (liveBean == null) { // add new relationship (Map not allowed here) liveVal.addBean(targetDescriptor.createReference(id, null)); } } else { // recursively publish the OneToMany child bean T newLive = targetDescriptor.publish(bean, liveBean); if (liveBean == null) { // Map not allowed here liveVal.addBean(newLive); } } } // anything remaining should be deleted (so remove from modify aware collection) Collection<T> values = liveBeansAsMap.values(); for (T value : values) { liveVal.removeBean(value); } } @SuppressWarnings("unchecked") private Map<Object, T> liveBeansAsMap(BeanCollection<?> liveVal) { liveVal.size(); Collection<?> liveBeans = liveVal.getActualDetails(); Map<Object, T> liveMap = new LinkedHashMap<>(); for (Object liveBean : liveBeans) { Object id = targetDescriptor.getId((EntityBean) liveBean); liveMap.put(id, (T) liveBean); } return liveMap; } public boolean isIncludeCascadeSave() { // Note ManyToMany always included as we always 'save' // the relationship via insert/delete of intersection table // REMOVALS means including PrivateOwned relationships return cascadeInfo.isSave() || hasJoinTable() || ModifyListenMode.REMOVALS == modifyListenMode; } public boolean isIncludeCascadeDelete() { return cascadeInfo.isDelete() || hasJoinTable() || ModifyListenMode.REMOVALS == modifyListenMode; } boolean isCascadeDeleteEscalate() { return !elementCollection && cascadeInfo.isDelete(); } public SpiSqlUpdate insertElementCollection() { return sqlHelp.insertElementCollection(); } public boolean isTargetDocStoreMapped() { return targetDescriptor.isDocStoreMapped(); } void jsonWriteMapEntry(SpiJsonWriter ctx, Map.Entry<?, ?> entry) throws IOException { if (elementDescriptor != null) { elementDescriptor.jsonWriteMapEntry(ctx, entry); } else { targetDescriptor.jsonWrite(ctx, (EntityBean)entry.getValue()); } } void jsonWriteElementValue(SpiJsonWriter ctx, Object element) { elementDescriptor.jsonWriteElement(ctx, element); }
Only cache Many Ids if the target bean is also cached.
/** * Only cache Many Ids if the target bean is also cached. */
public boolean isUseCache() { return targetDescriptor.isBeanCaching(); }
A Many (element collection) property in bean cache to held as JSON.
/** * A Many (element collection) property in bean cache to held as JSON. */
@Override public void setCacheDataValue(EntityBean bean, Object cacheData, PersistenceContext context) { try { String asJson = (String) cacheData; if (asJson != null && !asJson.isEmpty()) { Object collection = jsonReadCollection(asJson); setValue(bean, collection); } } catch (Exception e) { logger.error("Error setting value from L2 cache", e); } } @Override public Object getCacheDataValue(EntityBean bean) { try { Object collection = getValue(bean); if (collection == null) { return null; } return jsonWriteCollection(collection); } catch (Exception e) { logger.error("Error building value element collection json for L2 cache", e); return null; } }
Write the collection to JSON.
/** * Write the collection to JSON. */
public String jsonWriteCollection(Object value) throws IOException { StringWriter writer = new StringWriter(300); SpiJsonWriter ctx = descriptor.createJsonWriter(writer); help.jsonWrite(ctx, null, value, true); ctx.flush(); return writer.toString(); }
Read the collection as JSON.
/** * Read the collection as JSON. */
private Object jsonReadCollection(String json) throws IOException { SpiJsonReader ctx = descriptor.createJsonReader(json); JsonParser parser = ctx.getParser(); JsonToken event = parser.nextToken(); if (JsonToken.VALUE_NULL == event) { return null; } return jsonReadCollection(ctx, null); }
Write the collection to JSON.
/** * Write the collection to JSON. */
private void jsonWriteCollection(SpiJsonWriter ctx, String name, Object value, boolean explicitInclude) throws IOException { help.jsonWrite(ctx, name, value, explicitInclude); }
Read the collection (JSON Array) containing entity beans.
/** * Read the collection (JSON Array) containing entity beans. */
public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException { if (elementDescriptor != null && elementDescriptor.isJsonReadCollection()) { return elementDescriptor.jsonReadCollection(readJson, parentBean); } BeanCollection<?> collection = createEmpty(parentBean); BeanCollectionAdd add = getBeanCollectionAdd(collection); do { EntityBean detailBean = (EntityBean) targetDescriptor.jsonRead(readJson, name); if (detailBean == null) { // read the entire array break; } add.addEntityBean(detailBean); if (parentBean != null && childMasterProperty != null) { // bind detail bean back to master via mappedBy property childMasterProperty.setValue(detailBean, parentBean); } } while (true); return collection; }
Bind all the property values to the SqlUpdate.
/** * Bind all the property values to the SqlUpdate. */
public void bindElementValue(SqlUpdate insert, Object value) { targetDescriptor.bindElementValue(insert, value); } }