Page 1 of 1

It can't be this hard... can it?

Posted: Thu Apr 05, 2012 2:00 pm
by jearles
I have an entity that represents a store. A store has a list of product catalogs. A product catalog has a list of products. I have created a method on store that is able to return the distinct set of products that are available in the catalogs. A store also has a Many-to-One relationship to a "defaultProduct". As part of the Store presenter I would like to let an administrator select one of the available products to be the "default product". I can add an additional foreign key to the product inside of the store data source, but (for the life of me) I cannot figure out how to create the data source to present the list of possible products.

Help me Broadleaf Commerce forum... you're my only hope! ;)

Re: It can't be this hard... can it?

Posted: Tue Apr 10, 2012 12:01 pm
by phillipuniverse
This one is a little tricky, and definitely non-obvious. But possible!

You're right, you should add a foreign key to the Store back to Product. In order for the admin to work properly this is often the case (adding the bidrectional relationship). So, I'm going to assume you have a Store that looks something like this:

Code: Select all

@Entity
@AdminPresentationClass(populateToOneFields=PopulateToOneFieldsEnum.TRUE)
public class StoreImpl implements Serializable {
    //other properties omitted

    @ManyToOne(targetEntity = ProductImpl.class, optional=false)
    @JoinColumn(name="PRODUCT_ID")
    //make this field excluded, which will prevent any ToOne form inclusion. This is only needed if you have StoreImpl annotated with @AdminPresentationClass(populateToOneFields=PopulateToOneFieldsEnum.TRUE)
    @AdminPresentation(friendlyName="Default Product", excluded=true, fieldType=SupportedFieldType.FOREIGN_KEY)
    protected Product defaultProduct;

}
 


What this will do on the admin is create a form item with a '...' button that has the name "Default Product" when creating a new Store. Of course, this button isn't hooked up to anything yet and you'll get a GWT exception if you try to click on it.
Let's now put a FormItemCallback on the Store datasource and set up the ProductSearchDataSource:

in StorePresenter.java:

Code: Select all


public class StorePresenter extends DynamicEntityPresenter implements Instantiable 
{

//Other methods omitted

    @Override
    public void setup
()
        //I assume this is how you're setting up the Store datasource
        getPresenterSequenceSetupManager().addOrReplaceItem(new PresenterSetupItem("storeDS", new StoreDataSourceFactory(), new AsyncCallbackAdapter() {
            public void onSetupSuccess(DataSource top) {
                setupDisplayItems(top);
                ((ListGridDataSource) top).setupGridFields(new String[]{"storeField1", "storeField2", "storeField3"}, new Boolean[]{false, true, true});
            }
        }));

        //'productSearch' is an arbitrary name
        //Notice that the 3rd OperationTypes parameter is OperationType.JOINSTRUCTURE which tells the add process that it needs to be a join
        getPresenterSequenceSetupManager().addOrReplaceItem(new PresenterSetupItem("productSearchDS", new ProductDataSourceFactory(), new OperationTypes(OperationType.ENTITY, OperationType.ENTITY, OperationType.JOINSTRUCTURE, OperationType.ENTITY, OperationType.ENTITY), new Object[]{}, new AsyncCallbackAdapter() {
            public void onSetupSuccess(DataSource result) {
                ListGridDataSource productSearchDataSource = (ListGridDataSource) result;
//change these field names to whichever fields you want to be displayed when the search happens. If you leave this blank, it will build a form based on the @AdminPresentation annotations you have on Product
                productSearchDataSource.resetPermanentFieldVisibility(
                    "name",
                    "description"
                );
                                //usually you want the autofetch to true here; otherwise you'll have to hit the funnel button on the search
                EntitySearchDialog productSearchView = new EntitySearchDialog(productSearchDataSource, true);
                         
                                
//Here's where the real magic happens.  This will add a proper callback to the '...' button on the defaultProduct property from 'storeDS' (which was instantiated above). 
                getPresenterSequenceSetupManager().getDataSource("storeDS").getFormItemCallbackHandlerManager().addSearchFormItemCallback(
                    "defaultProduct", 
                    productSearchView
, 
                    
"Search for a Product",
                    getDisplay().getDynamicFormDisplay()
                );
            }
        }));


And you're done. I think your original question was about the DataSourceFactory. In this example, I called it ProductDataSourceFactory() which is just a simple call to pull back all of the Products:

Code: Select all


public class ProductDataSourceFactory implements DataSourceFactory 
{
    
    public static ListGridDataSource dataSource 
= null;
    
    public void createDataSource
(String name, OperationTypes operationTypes, Object[] additionalItems, AsyncCallback<DataSource> cb) {
        if (dataSource == null) {
            operationTypes = new OperationTypes(OperationType.ENTITY, OperationType.ENTITY, OperationType.ENTITY, OperationType.ENTITY, OperationType.ENTITY);
            PersistencePerspective persistencePerspective = new PersistencePerspective(operationTypes, new String[]{}, new ForeignKey[]{});
            DataSourceModule[] modules = new DataSourceModule[]{
                new BasicClientEntityModule(CeilingEntities.PRODUCT, persistencePerspective, AppServices.DYNAMIC_ENTITY)
            };
            dataSource = new ListGridDataSource(name, persistencePerspective, AppServices.DYNAMIC_ENTITY, modules);
            dataSource.buildFields(null, false, cb);
        } else {
            if (cb != null) {
                cb.onSuccess(dataSource);
            }
        }
    }
}


One caveat. I don't think this is necessary because in the annotation for defaultProduct, but you might have to do this in StoreDataSourceFactory:

Code: Select all


public class StoreDataSourceFactory implements DataSourceFactory 
{
    
    public static ListGridDataSource dataSource 
= null;
    
    public void createDataSource
(String name, OperationTypes operationTypes, Object[] additionalItems, AsyncCallback<DataSource> cb) {
        if (dataSource == null) {
            operationTypes = new OperationTypes(OperationType.ENTITY, OperationType.ENTITY, OperationType.ENTITY, OperationType.ENTITY, OperationType.ENTITY);
            PersistencePerspective persistencePerspective = new PersistencePerspective(operationTypes, new String[]{}, new ForeignKey[]{new ForeignKey("defaultProduct", EntityImplementations.PRODUCT, null)});
            DataSourceModule[] modules = new DataSourceModule[]{
                new BasicClientEntityModule(MyCompanyCeilingEntities.STORE, persistencePerspective, AppServices.DYNAMIC_ENTITY)
            };
            dataSource = new ListGridDataSource(name, persistencePerspective, AppServices.DYNAMIC_ENTITY, modules);
            dataSource.buildFields(null, false, cb);
        } else {
            if (cb != null) {
                cb.onSuccess(dataSource);
            }
        }
    }

}


But like I said, I'm not sure if that part is necessary.

Hopefully that helps. Let me know if you have any questions about any of that; it's kind of a long response.

Re: It can't be this hard... can it?

Posted: Tue Apr 10, 2012 12:03 pm
by phillipuniverse
And one more thing I wanted to ask. While I believe this should solve your problem, why not just use the provided Category functionality? You can always subclass CategoryImpl to provide additional properties that you might need. From what I read here in your design, that might be easier to do that having the separate entities that you described.

Re: It can't be this hard... can it?

Posted: Tue Apr 10, 2012 8:01 pm
by jearles
Hi pverheyden,

I appreciate your input, but I don't think your example exactly covers the scenario I require. If it was choose a defaultProduct from ALL available products I wouldn't have a problem. I have plenty of Many-to-One relationships that work this way and I do follow the pattern exactly as you describe. My problem is that I want to only to list the subset of products that are associated with the Product Catalogs that are associated with this specific store.

Product * -----* ProductCatalog *-----* Store

So the "main" entry is Store. I now want to create a DataSource that is able to list the union of all products that are associated with the product catalogs associated with the selected Store. I need to traverse two many-to-many relationships.

We are already using Category to determine what "departments" of the store a Product belongs to (e.g. Kitchenware), and we didn't want to overload Category by adding a seperate Category hierarchy to tag the Catalogs a product belongs to. Even if we were using category I believe we would have a similar issue, where I want to list only the products that were categorized by the SPECIFIC categories that were applicable to the current store. Stores vary in the "catalogs" they carry, but many stores can share the same catalog - which is why we introduced the catalog concept (to allow CRUD actions on a single entity that will affect many stores).

If I have misunderstood your example please let me know.

I appreciate any discussion, as we are slowly developing a series of common Presenter "patterns" over time to cover our domain. It seems to me that the open-admin (at its current level of development) is best suited for relatively simple CRUD operations between directly associated entities (or through a single intersection entity). I am sure that GWT / Smart-GWT must be able to handle more complex scenarios, but finding documentation on how to tie all the pieces together is proving a challenge.

Thanks again,

- John

Re: It can't be this hard... can it?

Posted: Wed Apr 11, 2012 10:23 am
by phillipuniverse
Ah I see; I understand fully now. You want the search to only contain Products within the ProductCatalogs that are already related to the particular Store. That's definitely a little more tricky. Let me think about that for a bit and get back to you.

Re: It can't be this hard... can it?

Posted: Thu Apr 12, 2012 10:51 am
by phillipuniverse
Ok, I think I have a good solution for this. This hinges on writing your own CustomPersistenceHandler. This class would extend from CustomPersistenceHandlerAdapter which by default returns false for canHandleAdd, canHandleUpdate, canHandleRemove, canHandleFetch. In your scenario, I would imagine something like this:

Code: Select all


public class ProductCustomPersistenceHandler extends CustomPersistenceHandlerAdapter 
{
    
    private static final Log LOG 
= LogFactory.getLog(ProductCustomPersistenceHandler.class);

    @Resource
    private ProductService productService
;
    @Resource
    private ProductCatalogService productCatalogService
;

    public Boolean canHandleFetch(PersistencePackage persistencePackage) {
        String ceilingEntityFullyQualifiedClassname = persistencePackage.getCeilingEntityFullyQualifiedClassname();
        String[] customCriteria = persistencePackage.getCustomCriteria();
        return !ArrayUtils.isEmpty(customCriteria) && "productCatalogProductSearch".equals(customCriteria[0]) && Product.class.getName().equals(ceilingEntityFullyQualifiedClassname);
    }

    public DynamicResultSet fetch(PersistencePackage persistencePackage, CriteriaTransferObject cto, DynamicEntityDao dynamicEntityDao, RecordHelper helper) throws ServiceException {
        String ceilingEntityFullyQualifiedClassname = persistencePackage.getCeilingEntityFullyQualifiedClassname();
        try {
            PersistencePerspective persistencePerspective = persistencePackage.getPersistencePerspective();
            Map<String, FieldMetadata> productProperties = helper.getSimpleMergedProperties(Product.class.getName(), persistencePerspective);
            BaseCtoConverter ctoConverter = helper.getCtoConverter(persistencePerspective, cto, Product.class.getName(), productProperties);        
            
//From within here, you can essentially do whatever you want to return the records that you need. Use JPA CriteriaBuilder, inject a custom service and call that, whatever
            //If you go the CriteriaBuilder route, you can get an instance of it by doing: dynamicEntityDao.getStandardEntityManager().getCriteriaBuilder();
        
            
//Get the criteria for the storeId
            String storeId = persistencePackage.getCustomCriteria()[1];
            ArrayList<Product> products = new ArrayList<Product>();
            if (storeId != null) {
                List<ProductCatalog> catalogs = productCatalogService.getProductCatalogsForStore(Long.parseLong(storeId));
                for (ProductCatalog catalog : catalogs) {
                    products.addAll(productService.getProductsForCatalog(catalog.getId());
                }
            }

            Entity[] entities = helper.getRecords(productProperties, products);
            return new DynamicResultSet(entities, entities.length);
        } catch (Exception e) {
            LOG.error("Unable to execute persistence activity", e);
            throw new ServiceException("Unable to perform fetch for entity: "+ceilingEntityFullyQualifiedClassname, e);
        }
    }
 


Then you can register this persistence handler in the blCustomPersistenceHandlers bean:

Code: Select all

<bean id="blCustomPersistenceHandlers" class="org.springframework.beans.factory.config.ListFactoryBean" scope="prototype">
    <property name="sourceList">
        <list>
            <bean class="com.mycompany.admin.server.service.handler.ProductCustomPersistenceHandler"/>
        </list>
    </property>
</bean>


Then your DataSourceFactory would look like this:

Code: Select all


public class CatalogProductsDataSourceFactory implements DataSourceFactory 
{
    
    public static CustomCriteriaListGridDataSource dataSource 
= null;
    
    public void createDataSource
(String name, OperationTypes operationTypes, Object[] additionalItems, AsyncCallback<DataSource> cb) {
        if (dataSource == null) {
            operationTypes = new OperationTypes(OperationType.ENTITY, OperationType.ENTITY, OperationType.ENTITY, OperationType.ENTITY, OperationType.ENTITY);
            PersistencePerspective persistencePerspective = new PersistencePerspective(operationTypes, new String[]{}, new ForeignKey[]{});
            DataSourceModule[] modules = new DataSourceModule[]{
                new BasicClientEntityModule(CeilingEntities.PRODUCT, persistencePerspective, AppServices.DYNAMIC_ENTITY)
            };
            dataSource = new CustomCriteriaListGridDataSource(name, persistencePerspective, AppServices.DYNAMIC_ENTITY, modules);
            dataSource.setCustomCriteria(new String[]{"productCatalogProductSearch", null});
            dataSource.buildFields(null, false, cb);
        } else {
            if (cb != null) {
                cb.onSuccess(dataSource);
            }
        }
    }
}
 


Also, check out the 'canHandleFetch' method in that custom persistence handler. I have it rely on a custom criteria so that the particular persistence handler is only used in specific instances, and it's not called every time I'm trying to fetch something with Product.class. You can see this when I instantiate a CustomCriteriaListGridDataSource instead of just a regular ListGridDataSource.

Now the only thing that's left is to actually change the store ID based on which store was selected, as well as a very slight change in the previous answer of setting up the DataSourceFactory. In StorePresenter:

Code: Select all


public class StorePresenter extends DynamicEntityPresenter implements Instantiable 
{
    //other properties omitted
    private CustomCriteriaListGridDataSource productCatalogProductDS;

    protected void changeSelection(Record selectedRecord) {
        String storeId = selectedRecord.getAttribute("id");
        if (productCatalogProductDS != null) {
            productCatalogProductDS.setCustomCriteria(new String[]{productCatalogProductDS.getCustomCriteria[0], storeId});
        }
    }

    protected void setup() {
        getPresenterSequenceSetupManager().addOrReplaceItem(new PresenterSetupItem("productSearchDS", new CatalogProductsDataSourceFactory(), new AsyncCallbackAdapter() {
            public void onSetupSuccess(DataSource result) {
                CustomCriteriaListGridDataSource productSearchDataSource = (CustomCriteriaListGridDataSource) result;

                productCatalogProductDS = productSearchDataSource;

                //change these field names to whichever fields you want to be displayed when the search happens. If you leave this blank, it will build a form based on the @AdminPresentation annotations you have on Product
                productSearchDataSource.resetPermanentFieldVisibility(
                    "name",
                    "description"
                );
                //usually you want the autofetch to true here; otherwise you'll have to hit the funnel button on the search
                EntitySearchDialog productSearchView = new EntitySearchDialog(productSearchDataSource, true);
                         
                 
//Here's where the real magic happens.  This will add a proper callback to the '...' button on the defaultProduct property from 'storeDS' (which was instantiated above). 
                getPresenterSequenceSetupManager().getDataSource("storeDS").getFormItemCallbackHandlerManager().addSearchFormItemCallback(
                    "defaultProduct", 
                    productSearchView
, 
                    
"Search for a Product",
                    getDisplay().getDynamicFormDisplay()
                );
            }
        }
    }
 


Something like that should be able to work. I think that that new property productCatalogProductDS might not be able to be set directly within the onSetupSuccess method. If that's the case, just create a setter for it and call the setter via onSetupSuccess. Let me know if you have any questions.

Re: It can't be this hard... can it?

Posted: Thu Apr 12, 2012 2:56 pm
by jearles
Thank you very much, Phillip!

This looks very promising. I will wire this up this afternoon and report back. I especially like the technique used to add criteria to the datasource and how to change it when a selection is made. Brilliant!

EDIT: Everything is looking good! I've got a couple of tweaks to make, but this works fabulously! Thank you, again!!

Re: It can't be this hard... can it?

Posted: Thu Apr 12, 2012 4:53 pm
by phillipuniverse
Awesome! Happy to help!

Re: It can't be this hard... can it?

Posted: Fri Apr 13, 2012 9:07 am
by jearles
Just to help anyone who may come along in the future... I had to change one minor thing when I declared my datasource.

Code: Select all

dataSource = new CustomCriteriaListGridDataSource(name, persistencePerspective,
     AppServices.DYNAMIC_ENTITY, modules, true, false, false, false, false);


If I did not pass in the "useForFetch" flag of 'true' then the datasource would always use the criteria passed to the executeFetch call (which was null) rather than the one we were changing (on the datasource).

Re: It can't be this hard... can it?

Posted: Sat Apr 14, 2012 6:09 pm
by phillipuniverse
Ah yes; good catch. Thanks for reporting back.