This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch 8.0.x-hibernate7-dev in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit f2427ba3fae0e0b0b1771337e1eef35800899607 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Tue Mar 3 09:40:33 2026 -0600 Extract bindOrderBy into CollectionOrderByBinder --- .../cfg/domainbinding/binder/CollectionBinder.java | 4 +- .../secondpass/CollectionOrderByBinder.java | 73 ++++++++++ .../secondpass/CollectionSecondPassBinder.java | 48 +----- .../secondpass/CollectionOrderByBinderSpec.groovy | 161 +++++++++++++++++++++ .../CollectionSecondPassBinderSpec.groovy | 97 +------------ 5 files changed, 246 insertions(+), 137 deletions(-) diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.java index 319d9b12c0..93648444f9 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.java @@ -30,6 +30,7 @@ import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentP import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty; import org.grails.orm.hibernate.cfg.domainbinding.secondpass.BidirectionalOneToManyLinker; import org.grails.orm.hibernate.cfg.domainbinding.secondpass.CollectionKeyColumnUpdater; +import org.grails.orm.hibernate.cfg.domainbinding.secondpass.CollectionOrderByBinder; import org.grails.orm.hibernate.cfg.domainbinding.secondpass.CollectionSecondPassBinder; import org.grails.orm.hibernate.cfg.domainbinding.secondpass.CollectionWithJoinTableBinder; import org.grails.orm.hibernate.cfg.domainbinding.secondpass.DependentKeyValueBinder; @@ -114,7 +115,8 @@ public class CollectionBinder { collectionWithJoinTableBinder, collectionForPropertyConfigBinder, new DefaultColumnNameFetcher(namingStrategy), - simpleValueColumnBinder); + simpleValueColumnBinder, + new CollectionOrderByBinder()); this.listSecondPassBinder = new ListSecondPassBinder( metadataBuildingContext, diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionOrderByBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionOrderByBinder.java new file mode 100644 index 0000000000..2adc916c07 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionOrderByBinder.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.cfg.domainbinding.secondpass; + +import java.util.Optional; +import java.util.Set; +import org.grails.datastore.mapping.model.DatastoreConfigurationException; +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity; +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateOneToManyProperty; +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty; +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty; +import org.grails.orm.hibernate.cfg.domainbinding.util.OrderByClauseBuilder; +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.PersistentClass; + +/** Binds the order-by clause and discriminator where condition to a collection. */ +public class CollectionOrderByBinder { + + private final OrderByClauseBuilder orderByClauseBuilder; + + /** Creates a new {@link CollectionOrderByBinder} instance. */ + public CollectionOrderByBinder() { + this.orderByClauseBuilder = new OrderByClauseBuilder(); + } + + /** Binds the order-by clause and discriminator where condition to the given collection. */ + public void bind( + HibernateToManyProperty property, + Collection collection, + PersistentClass associatedClass) { + GrailsHibernatePersistentEntity referenced = property.getHibernateAssociatedEntity(); + + if (referenced.isTablePerHierarchySubclass()) { + String discriminatorColumnName = referenced.getDiscriminatorColumnName(); + Set<String> discSet = referenced.buildDiscriminatorSet(); + String clause = String.join(",", discSet); + collection.setWhere(discriminatorColumnName + " in (" + clause + ")"); + } + + if (property.hasSort()) { + if (!property.isBidirectional() && property instanceof HibernateOneToManyProperty) { + throw new DatastoreConfigurationException( + "Default sort for associations [" + + property.getHibernateOwner().getName() + + "->" + + property.getName() + + "] are not supported with unidirectional one to many relationships."); + } + HibernatePersistentProperty sortBy = + (HibernatePersistentProperty) referenced.getPropertyByName(property.getSort()); + String order = Optional.ofNullable(property.getOrder()).orElse("asc"); + collection.setOrderBy( + orderByClauseBuilder.buildOrderByClause( + sortBy.getName(), associatedClass, collection.getRole(), order)); + } + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java index 9a34c98add..83410c5573 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java @@ -23,20 +23,15 @@ import static org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBind import jakarta.annotation.Nonnull; import java.util.*; import java.util.Map; -import java.util.Set; -import org.grails.datastore.mapping.model.DatastoreConfigurationException; import org.grails.datastore.mapping.model.config.GormProperties; import org.grails.orm.hibernate.cfg.domainbinding.binder.CollectionForPropertyConfigBinder; import org.grails.orm.hibernate.cfg.domainbinding.binder.ManyToOneBinder; import org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateManyToManyProperty; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateOneToManyProperty; -import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateManyToOneProperty; -import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity; import org.grails.orm.hibernate.cfg.domainbinding.util.DefaultColumnNameFetcher; -import org.grails.orm.hibernate.cfg.domainbinding.util.OrderByClauseBuilder; import org.hibernate.MappingException; import org.hibernate.boot.spi.InFlightMetadataCollector; import org.hibernate.mapping.*; @@ -51,7 +46,7 @@ public class CollectionSecondPassBinder { private static final Logger LOG = LoggerFactory.getLogger(CollectionSecondPassBinder.class); private final DefaultColumnNameFetcher defaultColumnNameFetcher; - private final OrderByClauseBuilder orderByClauseBuilder; + private final CollectionOrderByBinder collectionOrderByBinder; private final ManyToOneBinder manyToOneBinder; private final PrimaryKeyValueCreator primaryKeyValueCreator; private final CollectionKeyColumnUpdater collectionKeyColumnUpdater; @@ -73,7 +68,8 @@ public class CollectionSecondPassBinder { CollectionWithJoinTableBinder collectionWithJoinTableBinder, CollectionForPropertyConfigBinder collectionForPropertyConfigBinder, DefaultColumnNameFetcher defaultColumnNameFetcher, - SimpleValueColumnBinder simpleValueColumnBinder) { + SimpleValueColumnBinder simpleValueColumnBinder, + CollectionOrderByBinder collectionOrderByBinder) { this.manyToOneBinder = manyToOneBinder; this.primaryKeyValueCreator = primaryKeyValueCreator; this.collectionKeyColumnUpdater = collectionKeyColumnUpdater; @@ -84,7 +80,7 @@ public class CollectionSecondPassBinder { this.collectionForPropertyConfigBinder = collectionForPropertyConfigBinder; this.defaultColumnNameFetcher = defaultColumnNameFetcher; this.simpleValueColumnBinder = simpleValueColumnBinder; - this.orderByClauseBuilder = new OrderByClauseBuilder(); + this.collectionOrderByBinder = collectionOrderByBinder; } /** Bind collection second pass. */ @@ -95,7 +91,7 @@ public class CollectionSecondPassBinder { @Nonnull Collection collection) { PersistentClass associatedClass = resolveAssociatedClass(property, persistentClasses); - bindOrderBy(property, collection, associatedClass); + collectionOrderByBinder.bind(property, collection, associatedClass); bindOneToManyAssociation(property, associatedClass, collection); applyMultiTenantFilter(property, collection); @@ -227,36 +223,4 @@ public class CollectionSecondPassBinder { .orElseThrow(() -> new MappingException("Association [" + property.getName() + "] has no associated class")); } - private void bindOrderBy( - HibernateToManyProperty property, Collection collection, PersistentClass associatedClass) { - if (associatedClass == null) { - return; - } - GrailsHibernatePersistentEntity referenced = property.getHibernateAssociatedEntity(); - - if (referenced.isTablePerHierarchySubclass()) { - String discriminatorColumnName = referenced.getDiscriminatorColumnName(); - Set<String> discSet = referenced.buildDiscriminatorSet(); - String inclause = String.join(",", discSet); - - collection.setWhere(discriminatorColumnName + " in (" + inclause + ")"); - } - - if (property.hasSort()) { - if (!property.isBidirectional() && property instanceof HibernateOneToManyProperty) { - throw new DatastoreConfigurationException( - "Default sort for associations [" - + property.getHibernateOwner().getName() - + "->" - + property.getName() - + "] are not supported with unidirectional one to many relationships."); - } - HibernatePersistentProperty sortBy = - (HibernatePersistentProperty) referenced.getPropertyByName(property.getSort()); - String order = Optional.ofNullable(property.getOrder()).orElse("asc"); - collection.setOrderBy( - orderByClauseBuilder.buildOrderByClause( - sortBy.getName(), associatedClass, collection.getRole(), order)); - } - } -} +} \ No newline at end of file diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionOrderByBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionOrderByBinderSpec.groovy new file mode 100644 index 0000000000..87a8819c0c --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionOrderByBinderSpec.groovy @@ -0,0 +1,161 @@ +package org.grails.orm.hibernate.cfg.domainbinding.secondpass + +import grails.gorm.annotation.Entity +import grails.gorm.specs.HibernateGormDatastoreSpec +import org.grails.datastore.mapping.model.DatastoreConfigurationException +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty +import org.hibernate.mapping.Bag +import org.hibernate.mapping.BasicValue +import org.hibernate.mapping.Column +import org.hibernate.mapping.Property +import org.hibernate.mapping.RootClass +import org.hibernate.mapping.Table +import spock.lang.Subject + +class CollectionOrderByBinderSpec extends HibernateGormDatastoreSpec { + + @Subject + CollectionOrderByBinder binder = new CollectionOrderByBinder() + + void setupSpec() { + manager.addAllDomainClasses([ + COBOwnerEntity, + COBAssociatedItem, + COBUnidirectionalOwner, + COBBaseItem, + COBSubItem, + COBHierarchyOwner, + ]) + } + + private HibernateToManyProperty propertyFor(Class ownerClass, String name = "items") { + (getPersistentEntity(ownerClass) as GrailsHibernatePersistentEntity).getPropertyByName(name) as HibernateToManyProperty + } + + private RootClass rootClassWith(String entityName, String propertyName, String columnName) { + def mbc = getGrailsDomainBinder().getMetadataBuildingContext() + def rootClass = new RootClass(mbc) + rootClass.setEntityName(entityName) + def table = new Table("test", entityName.toLowerCase()) + def simpleValue = new BasicValue(mbc, table) + simpleValue.setTypeName("string") + simpleValue.addColumn(new Column(columnName)) + def prop = new Property() + prop.setName(propertyName) + prop.setValue(simpleValue) + rootClass.addProperty(prop) + return rootClass + } + + def "bind sets orderBy when sort is configured on a bidirectional association"() { + given: + def property = propertyFor(COBOwnerEntity) + def collection = new Bag(getGrailsDomainBinder().getMetadataBuildingContext(), null) + collection.setRole("${COBOwnerEntity.name}.items") + def associatedClass = rootClassWith(COBAssociatedItem.name, "value", "VALUE") + property.getMappedForm().setSort("value") + property.getMappedForm().setOrder("desc") + + when: + binder.bind(property, collection, associatedClass) + + then: + collection.getOrderBy() != null + collection.getOrderBy().contains("desc") + } + + def "bind defaults to asc when order is not specified"() { + given: + def property = propertyFor(COBOwnerEntity) + def collection = new Bag(getGrailsDomainBinder().getMetadataBuildingContext(), null) + collection.setRole("${COBOwnerEntity.name}.items") + def associatedClass = rootClassWith(COBAssociatedItem.name, "value", "VALUE") + property.getMappedForm().setSort("value") + + when: + binder.bind(property, collection, associatedClass) + + then: + collection.getOrderBy() != null + collection.getOrderBy().contains("asc") + } + + def "bind throws DatastoreConfigurationException for unidirectional one-to-many with sort"() { + given: + def property = propertyFor(COBUnidirectionalOwner) + def collection = new Bag(getGrailsDomainBinder().getMetadataBuildingContext(), null) + def associatedClass = new RootClass(getGrailsDomainBinder().getMetadataBuildingContext()) + property.getMappedForm().setSort("value") + + when: + binder.bind(property, collection, associatedClass) + + then: + thrown(DatastoreConfigurationException) + } + + def "bind does not set orderBy when no sort is configured"() { + given: + def property = propertyFor(COBOwnerEntity) + def collection = new Bag(getGrailsDomainBinder().getMetadataBuildingContext(), null) + def associatedClass = new RootClass(getGrailsDomainBinder().getMetadataBuildingContext()) + + when: + binder.bind(property, collection, associatedClass) + + then: + collection.getOrderBy() == null + } + + def "bind sets where clause for table-per-hierarchy subclass"() { + given: + def property = propertyFor(COBHierarchyOwner) + def collection = new Bag(getGrailsDomainBinder().getMetadataBuildingContext(), null) + def associatedClass = new RootClass(getGrailsDomainBinder().getMetadataBuildingContext()) + + when: + binder.bind(property, collection, associatedClass) + + then: + collection.getWhere() != null + collection.getWhere().contains("DTYPE in (") + collection.getWhere().contains("COBSubItem") + } +} + +@Entity +class COBOwnerEntity { + Long id + static hasMany = [items: COBAssociatedItem] +} + +@Entity +class COBAssociatedItem { + Long id + String value + COBOwnerEntity owner + static belongsTo = [owner: COBOwnerEntity] +} + +@Entity +class COBUnidirectionalOwner { + Long id + static hasMany = [items: COBAssociatedItem] +} + +@Entity +class COBBaseItem { + Long id + String value +} + +@Entity +class COBSubItem extends COBBaseItem { +} + +@Entity +class COBHierarchyOwner { + Long id + static hasMany = [items: COBSubItem] +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinderSpec.groovy index 9a4ee671f9..09f56705c9 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinderSpec.groovy @@ -5,10 +5,8 @@ import grails.gorm.specs.HibernateGormDatastoreSpec import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty -import org.hibernate.mapping.Bag import org.hibernate.mapping.RootClass -import org.grails.datastore.mapping.model.DatastoreConfigurationException import org.grails.datastore.mapping.model.PersistentEntity import org.grails.orm.hibernate.cfg.domainbinding.binder.ManyToOneBinder import org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueBinder @@ -43,7 +41,7 @@ class CollectionSecondPassBinderSpec extends HibernateGormDatastoreSpec { def dcnf = new DefaultColumnNameFetcher(ns, new BackticksRemover()) def svcb = new SimpleValueColumnBinder() - binder = new CollectionSecondPassBinder(mtob, pkvc, cku, botml, dkvb, uotmb, cwjtb, cfpcb, dcnf, svcb) + binder = new CollectionSecondPassBinder(mtob, pkvc, cku, botml, dkvb, uotmb, cwjtb, cfpcb, dcnf, svcb, new CollectionOrderByBinder()) } protected HibernatePersistentProperty createTestHibernateToManyProperty(Class<?> domainClass = CSPBTestEntityWithMany, String propertyName = "items") { @@ -52,63 +50,6 @@ class CollectionSecondPassBinderSpec extends HibernateGormDatastoreSpec { return property } - def "test bindOrderBy with sort configured"() { - given: - def property = createTestHibernateToManyProperty(CSPBTestEntityWithMany, "items") as HibernateToManyProperty - def collection = new Bag(getGrailsDomainBinder().getMetadataBuildingContext(), null) - collection.setRole("CSPBTestEntityWithMany.items") - - def associatedPersistentClass = new RootClass(getGrailsDomainBinder().getMetadataBuildingContext()) - associatedPersistentClass.setEntityName(CSPBAssociatedItem.name) - def valueProperty = new org.hibernate.mapping.Property() - valueProperty.setName("value") - def simpleValue = new org.hibernate.mapping.BasicValue(getGrailsDomainBinder().getMetadataBuildingContext(), new org.hibernate.mapping.Table("test", "ASSOCIATED_ITEM")) - simpleValue.setTypeName("string") - def column = new org.hibernate.mapping.Column("VALUE") - simpleValue.addColumn(column) - valueProperty.setValue(simpleValue) - associatedPersistentClass.addProperty(valueProperty) - - property.getMappedForm().setSort("value") - property.getMappedForm().setOrder("desc") - - when: - binder.bindOrderBy(property, collection, associatedPersistentClass) - - then: - collection.getOrderBy() != null - } - - def "test bindOrderBy with unidirectional one-to-many throws exception"() { - given: - def property = createTestHibernateToManyProperty(CSPBUnidirectionalEntity, "items") as HibernateToManyProperty - def collection = new Bag(getGrailsDomainBinder().getMetadataBuildingContext(), null) - def associatedPersistentClass = new RootClass(getGrailsDomainBinder().getMetadataBuildingContext()) - associatedPersistentClass.setEntityName(CSPBAssociatedItem.name) - - property.getMappedForm().setSort("value") - - when: - binder.bindOrderBy(property, collection, associatedPersistentClass) - - then: - thrown(DatastoreConfigurationException) - } - - def "test bindOrderBy does nothing without sort"() { - given: - def property = createTestHibernateToManyProperty(CSPBTestEntityWithMany, "items") as HibernateToManyProperty - def collection = new Bag(getGrailsDomainBinder().getMetadataBuildingContext(), null) - def associatedPersistentClass = new RootClass(getGrailsDomainBinder().getMetadataBuildingContext()) - associatedPersistentClass.setEntityName(CSPBAssociatedItem.name) - - when: - binder.bindOrderBy(property, collection, associatedPersistentClass) - - then: - collection.getOrderBy() == null - } - def "resolveAssociatedClass throws MappingException when property has no associated entity"() { given: def property = createTestHibernateToManyProperty(CSPBTestEntityWithMany, "items") as HibernateToManyProperty @@ -135,7 +76,7 @@ class CollectionSecondPassBinderSpec extends HibernateGormDatastoreSpec { ex.message.contains("items") } - def "test resolveAssociatedClass returns associatedClass"() { + def "resolveAssociatedClass returns the matching PersistentClass"() { given: def property = createTestHibernateToManyProperty(CSPBTestEntityWithMany, "items") as HibernateToManyProperty def associatedPersistentClass = new RootClass(getGrailsDomainBinder().getMetadataBuildingContext()) @@ -148,31 +89,6 @@ class CollectionSecondPassBinderSpec extends HibernateGormDatastoreSpec { then: result == associatedPersistentClass } - - def "test bindOrderBy with table per hierarchy subclass"() { - given: - def property = createTestHibernateToManyProperty(CSPBTestEntityWithMany, "items") as HibernateToManyProperty - def collection = new Bag(getGrailsDomainBinder().getMetadataBuildingContext(), null) - def associatedPersistentClass = new RootClass(getGrailsDomainBinder().getMetadataBuildingContext()) - associatedPersistentClass.setEntityName(CSPBAssociatedItem.name) - - // Mock GrailsHibernatePersistentEntity behavior for table per hierarchy - def referencedEntity = property.getHibernateAssociatedEntity() - def spiedReferencedEntity = Spy(referencedEntity) - spiedReferencedEntity.isTablePerHierarchySubclass() >> true - spiedReferencedEntity.getDiscriminatorColumnName() >> "item_type" - spiedReferencedEntity.buildDiscriminatorSet() >> (["'A'", "'B'"] as Set) - - // Inject the spy if possible, or mock the getter on property - def spiedProperty = Spy(property) - spiedProperty.getHibernateAssociatedEntity() >> spiedReferencedEntity - - when: - binder.bindOrderBy(spiedProperty, collection, associatedPersistentClass) - - then: - collection.getWhere() == "item_type in ('A','B')" - } } @Entity @@ -186,13 +102,6 @@ class CSPBTestEntityWithMany { class CSPBAssociatedItem { Long id String value - CSPBTestEntityWithMany parent // Bidirectional for association property testing + CSPBTestEntityWithMany parent static belongsTo = [parent: CSPBTestEntityWithMany] } - -@Entity -class CSPBUnidirectionalEntity { - Long id - Set<CSPBAssociatedItem> items - static hasMany = [items: CSPBAssociatedItem] -}
