Search code examples
broadleaf-commerce

Broadleaf 5.2 - How do I set ProductOptions into a newly created Product via the API?


Problem:

I am seeing that the Product "setProductOptions()" method has been deprecated, and that the "setProductOptionXrefs()" is preferred. The issue is that I can't seem to find any example of how to set the ProductOptionXrefs.

I've looked for examples within the Broadleaf "BroadleafCommerce-develop-5.2.x" and "DemoSite-broadleaf-5.2.2.1-GA" projects as well as combed the galaxy for an example. No luck.

The Goal (X):

I am creating an endpoint that will take in a JSON object and accept two parameters, (categoryName and price).

The endpoint will:

  • Find the category that the product will belong to.
  • Create a new Sku object and populate with some of the input wrapper fields.
  • Create a new Product object and populate a few wrapper fields.
  • Assign a ProductOption to this new Product. <== X
  • Save the Product.
  • Return a response (for the REST response).

The endpoint itself looks like:

@RequestMapping(value = "/my_product", method = RequestMethod.POST, consumes = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
public ProductWrapper addCSDLProduct(HttpServletRequest request, @RequestBody ProductWrapper wrapper,
                                 @RequestParam(value = "categoryName", required = true) String categoryName,
                                 @RequestParam(value = "price", required = true) double price) {

    Category category = null;
    List<Category> categories = catalogService.findCategoriesByName( categoryName );
    if ( categories != null && categories.size() > 0 ) {
        category = categories.get(0);
    }

    Sku defaultSku = catalogService.createSku();
    defaultSku.setRetailPrice(new Money( price ));
    defaultSku.setInventoryType( InventoryType.ALWAYS_AVAILABLE );
    defaultSku.setName( wrapper.getName() );
    defaultSku.setLongDescription( wrapper.getLongDescription() );
    defaultSku.setDescription( wrapper.getDescription() );
    defaultSku.setUrlKey( wrapper.getUrl() );
    defaultSku.setActiveStartDate( new Date() );

    Product product = catalogService.createProduct(ProductType.PRODUCT);
    product.setDefaultSku(defaultSku);        
    product.setUrl( wrapper.getUrl() );
    product.setCategory(category);  

    List<ProductOptionXref> productOptionXrefs = new ArrayList<ProductOptionXref>();
    List<ProductOption> allProductOptions = catalogService.readAllProductOptions();        
    if ( null != allProductOptions && allProductOptions.size() > 0 ) {
        for ( ProductOption po : allProductOptions ) {          
            String current = po.getName();
            if ( current.equalsIgnoreCase("Shirt Color") ) {
                ProductOptionXref productOptionXref = new ProductOptionXrefImpl();
                productOptionXref.setProductOption(po);
                productOptionXrefs.add(productOptionXref);
            }               
        }
    }

    product.setProductOptionXrefs(productOptionXrefs);

    Product finalProduct = catalogService.saveProduct(product);
    Long newId = finalProduct.getId();

    ProductWrapper response;
    response = (ProductWrapper) context.getBean(ProductWrapper.class.getName());
    response.wrapDetails(product, request);
    response.setId(newId);

    return response;
}

The input object (an example) I use is:

{
    "name": "This is the name of the product.",
    "description": "This is the description of the product.",
    "longDescription": "This is a long description of the product. Really long.",
    "url": "/this/is/the/url/of/the/product",
    "defaultSku": {
        "name": "This is the name of the product.",
        "active": true,
        "available": true,
        "inventoryType": "ALWAYS_AVAILABLE",
        "retailPrice": {
            "amount": 19.0,
            "currency": "USD"
        }
    }
}

With a catgoryName of "Merchandise" and a price of 19.00.

This code above is currently returning a "Not-null" error:

Not-null property references a transient value - transient instance must be saved before current operation: org.broadleafcommerce.core.catalog.domain.ProductOptionXrefImpl.product -> org.broadleafcommerce.core.catalog.domain.ProductImpl; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation: org.broadleafcommerce.core.catalog.domain.ProductOptionXrefImpl.product -> org.broadleafcommerce.core.catalog.domain.ProductImpl

which most likely has to do with my

if ( current.equalsIgnoreCase("Shirt Color") ) {
  ProductOptionXref productOptionXref = new ProductOptionXrefImpl();
  productOptionXref.setProductOption(po);
  productOptionXrefs.add(productOptionXref);
}   

and/or

Product finalProduct = catalogService.saveProduct(product);

lines just above.

I could (eventually) figure out the "Not-null" error, but the question I ask is if anyone has an example of adding a ProductOption or ProductOptionXref to a newly created Product?

Thanks

Jon

[UPDATE]

I wanted to update my question with the solution, much thanks to @phillipuniverse for the example/explanations.

@RequestMapping(value = "/my_product", method = RequestMethod.POST, consumes = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
public ProductWrapper addCSDLProduct(HttpServletRequest request, @RequestBody ProductWrapper wrapper,
                                 @RequestParam(value = "categoryName", required = true) String categoryName,
                                 @RequestParam(value = "price", required = true) double price) {

    Category category = null;
    List<Category> categories = catalogService.findCategoriesByName( categoryName );
    if ( categories != null && categories.size() > 0 ) {
        category = categories.get(0);
    }

    Sku defaultSku = catalogService.createSku();
    defaultSku.setRetailPrice(new Money( price ));
    defaultSku.setInventoryType( InventoryType.ALWAYS_AVAILABLE );
    defaultSku.setName( wrapper.getName() );
    defaultSku.setLongDescription( wrapper.getLongDescription() );
    defaultSku.setDescription( wrapper.getDescription() );
    defaultSku.setUrlKey( wrapper.getUrl() );
    defaultSku.setActiveStartDate( new Date() );

    Product product = catalogService.createProduct(ProductType.PRODUCT);
    product.setDefaultSku(defaultSku);        
    product.setUrl( wrapper.getUrl() );
    product.setCategory(category);  

    List<ProductOptionXref> productOptionXrefs = new ArrayList<ProductOptionXref>();
    List<ProductOption> allProductOptions = catalogService.readAllProductOptions();        
    if ( null != allProductOptions && allProductOptions.size() > 0 ) {
        for ( ProductOption po : allProductOptions ) {          
            String current = po.getName();
            if ( current.equalsIgnoreCase("Shirt Color") ) {
                ProductOptionXref productOptionXref = new ProductOptionXrefImpl();
                productOptionXref.setProductOption(po);
                productOptionXref.setProduct(product);
                productOptionXrefs.add(productOptionXref);
            }               
        }
    }

    product.setProductOptionXrefs(productOptionXrefs);

    Product finalProduct = catalogService.saveProduct(product);
    finalProduct.getDefaultSku().setDefaultProduct(finalProduct);
    catalogService.saveSku(finalProduct.getDefaultSku());        
    Long newId = finalProduct.getId();

    ProductWrapper response;
    response = (ProductWrapper) context.getBean(ProductWrapper.class.getName());
    response.wrapDetails(product, request);
    response.setId(newId);

    return response;
}

Jon


Solution

  • It looks like the majority of what you have there is correct. You are missing a back-reference with the default Sku though; admittedly it's a little weird. You've got this part right:

    Product product = catalogService.createProduct(ProductType.PRODUCT);
    product.setDefaultSku(defaultSku);
    

    But then you also need to make sure that the default sku references back to the product; you have the save the product first and then set it:

    ...
    Product finalProduct = catalogService.saveProduct(product);
    finalProduct.getDefaultSku().setDefaultProduct(finalProduct);
    catalogService.saveSku(finalProduct.getDefaultSku());
    

    This code above is currently returning a "Not-null" error:

    You are right, the problem is in this code, and it is because you are not setting the product property on the ProductOptionXrefImpl:

    if ( current.equalsIgnoreCase("Shirt Color") ) {
        ProductOptionXref productOptionXref = new ProductOptionXrefImpl();
        productOptionXref.setProductOption(po);
        productOptionXrefs.add(productOptionXref);
    }
    

    If you look at the product property in ProductOptionXrefImpl you will find this:

    @ManyToOne(targetEntity = ProductImpl.class, optional=false, cascade = CascadeType.REFRESH)
    @JoinColumn(name = "PRODUCT_ID")
    protected Product product = new ProductImpl();
    

    You are getting past the optional=false part, but the new ProductImpl() part makes it always be a 'transient' instance with Hibernate; it does not have an ID property and Hibernate doesn't know anything about it. Since the cascade is set to only REFRESH, Hibernate also doesn't try to persist it or anything (which it shouldn't).

    The fix is to set the product property on the option xref:

    if ( current.equalsIgnoreCase("Shirt Color") ) {
        ProductOptionXref productOptionXref = new ProductOptionXrefImpl();
        productOptionXref.setProductOption(po);
        productOptionXref.setProduct(product);
        productOptionXrefs.add(productOptionXref);
    }
    

    I think this will work, but you might have to move the product option creation to below your first save of the Product, and set it to finalProduct. But I think as-written all of the cascades will figure it out.