Currently Being Moderated

Attribute Value Restriction in Commerce Manager

Posted by dminor on Dec 1, 2009 4:04:44 PM

Elastic Path comes with a very flexible attribute system, allowing you to customize the attributes associated with products, skus, customers, categories, and more. Attributes can be expressed as several different types -- strings, lists of strings, integers, decimals, booleans, images, etc. -- so that a great variety of information can be associated with your domain objects.

 

Editing this information is fairly straightforward in the Commerce Manager client. However, if you'd like to restrict an attribute to certain values, there is currently no way to enforce this in the client, and the potential exists for entering data incorrectly. This can cause problems if you are relying on the value of a particular attribute for functionality on your website, or perhaps for interfacing with your back-end systems.

 

Fortunately, we have the EP source code and can implement this functionality with a little effort, using a list of options stored in the database and some modifications to the Commerce Manager Client.

 

Domain Objects

 

Attribute values are derived from com.elasticpath.domain.attribute.impl.AbstractAttributeValueImpl, which is a "mapped superclass", meaning that there is no superclass db table and subclasses exist separately in their own db table. Since we need to maintain a per-attribute list of legal attribute values, we can simply leverage this existing infrastructure:

 

@Entity
@Table(name = AttributeOptionImpl.TABLE_NAME)
public class AttributeOptionImpl extends AbstractAttributeValueImpl {
 
     /**
      * The name of the table & generator to use for persistence.
      */
     public static final String TABLE_NAME = "TATTRIBUTEOPTION";
     
     private long uidPk;
 
     /**
      * Gets the unique identifier for this domain model object.
      *
      * @return the unique identifier.
      */
     @Id
     @Column(name = "UIDPK")
     @GeneratedValue(strategy = GenerationType.TABLE, generator = TABLE_NAME)
     @TableGenerator(name = TABLE_NAME, table = "JPA_GENERATED_KEYS", pkColumnName = "ID", valueColumnName = "LAST_VALUE", pkColumnValue = TABLE_NAME)
     public long getUidPk() {
          return uidPk;
     }
 
     /**
      * Sets the unique identifier for this domain model object.
      *
      * @param uidPk the new unique identifier.
      */
     public void setUidPk(final long uidPk) {
          this.uidPk = uidPk;
     }
}

 

Because the superclass already defines the link to the Attribute as well as the fields for storing the actual value, there is nothing more to define in the subclass. Don't forget to create TATTRIBUTEOPTION and add an entry to JPA_GENERATED_KEYS!

 

These attribute value options will belong to the Attribute entity, so we will need a "join" in com.elasticpath.domain.attribute.impl.AttributeImpl (or a subclass to minimize EP code changes):

 

     private List<AttributeValue> attributeOptions;
 
 
     @OneToMany(targetEntity = AttributeOptionImpl.class,
                  cascade = { CascadeType.ALL })
     @ElementJoinColumn(name = "ATTRIBUTE_UID", nullable = false)
     @ElementDependent
     @ElementForeignKey
     public List<AttributeValue> getAttributeOptions() {
          return this.attributeOptions;
     }
     
     public void setAttributeOptions(final List<AttributeValue> attributeOptions) {
          this.attributeOptions = attributeOptions;
     }

 

These methods will need to be exposed via the Attribute interface as well. You will also need to modify the fetch groups you are using when retrieving attributes, either by adding attribute options to the 'attributes' fetch group, or for better performance as a separate fetch group that is included as needed in the CM Client (such as in CM Client's ProductEditorInput.retrieveProduct()).

 

User Interface

 

Now that the domain objects have been defined, we can turn our attention towards the user interface. Because the attributes were built with extensibility in mind, editing attributes has been somewhat modularized, making it easier for us to plug in our attribute option selection where appropriate.

 

Dialog for Selecting Among Options

 

The first thing we'll do is create a simple dialog for choosing among our attribute options (I've left out some boilerplate such as setting the title):

public class AttributeOptionDialog extends AbstractEpDialog implements IValueRetriever {
     /**
      * the attribute being edited.
      */
     private Attribute attr;
 
 
     /**
      * the value of the handled attribute.
      */
     private Object value;
 
     private CCombo comboBox;
     
     private List<AttributeValueWithType> comboValues;
     
     /**
      * The constructor of the attribute option dialog window.
      * 
      * @param parentShell the parent shell object of the dialog window
      * @param value the attribute value passed in
      */
     public AttributeOptionDialog(final Shell parentShell, final Object value, Attribute attr) {
          super(parentShell, 2, false);
          this.value = value;
          this.attr = attr;
     }
 
     /**
      * Create the set and cancel button on the dialog window.
      * 
      * @param buttonsBarType The button bar type object passed in.
      * @param parent the parent composite
      */
     @Override
     protected void createEpButtonsForButtonsBar(final ButtonsBarType buttonsBarType, final Composite parent) {
          createEpOkButton(parent, CoreMessages.AbstractEpDialog_ButtonOK, null);
          createEpCancelButton(parent);
     }
 
     /**
      * Create the content of integer dialog window.
      * 
      * @param dialogComposite The dialog composite object
      */
     @Override
     protected void createEpDialogContent(final IEpLayoutComposite dialogComposite) {
          this.comboBox = dialogComposite.addComboBox(EpState.EDITABLE, dialogComposite.createLayoutData(IEpLayoutData.FILL,
                    IEpLayoutData.CENTER, true, true));
          this.comboBox.setVisibleItemCount(20);
     }
 
     /**
      * Handle button pressed event.
      */
     @Override
     protected void okPressed() {
          if (comboBox.getSelectionIndex() >= 0 && comboBox.getSelectionIndex() < comboValues.size()) {
               this.value = comboValues.get(comboBox.getSelectionIndex()).getValue();
          }
          super.okPressed();
     }
 
     /**
      * Get the input value.
      * @return the value input by the user.
      */
     public Object getValue() {
          return this.value;
     }
 
     /**
      * populate the control created in the dialog window.
      */
     @Override
     protected void populateControls() {
          if (attr.getAttributeOptions() != null ) {
               comboValues = new ArrayList<AttributeValueWithType>();
               for( AttributeValue option : attr.getAttributeOptions()) {
                    comboValues.add((AttributeValueWithType)option);
               }
               
               Collections.sort(comboValues);
 
               if (value != null) {
                    int index = 0;
                    for(AttributeValueWithType option : comboValues) {
                         comboBox.add(option.getStringValue());
                         if (value.equals(option.getValue())) {
                              comboBox.select(index);
                         }
 
                         index++;
                    }
               } else {
                    for(AttributeValueWithType option : comboValues) {
                         comboBox.add(option.getStringValue());
                    }
               }
          }
     }
}

 

This dialog will present the user with a drop down list containing the attribute options, sorted according to their natural ordering. When OK is pressed, the dialog's value will be set to the selected attribute value.

 

Now we need to modify the attribute editing code in com.elasticpath.cmclient.catalog.editors.attribute, which is utilized throughout the CM Client UI. In AttributeEditingSupport.java we'll find the code for managing the dialogs and cell editors used for editing attributes.

 

There are two paths a user can take for editing an attribute's value: a user can select the attribute and press the edit button, which presents a dialog, or click in the table cell, which leads to the same dialog in most cases, but is directly editable in the case of short text values. For simplicity's sake we will present our dialog when the user takes either of the two actions (another approach, for a more seamless UI, might be to switch to a ComboBoxEditor to place a dropdown menu directly in the cell). One final exception to worry about is multi-valued short text values -- we will display a modified version of the ShortTextMultiValueDialog.

 

Most of the cell editors use a DialogCellEditor for editing their contents. We will add a check to see if the attribute has AttributeOptions, and open our dialog instead. To keep our EP code changes to a minimum, we will insert a new class into the hierarchy between DialogCellEditor and the individual cell editor classes, which checks for attribute options and opens our new dialog if they exist:

 

public abstract class AttributeDialogCellEditor extends DialogCellEditor {
 
     private final AttributeValue attr;
 
     protected AttributeDialogCellEditor(final Composite parent,
               final AttributeValue attr) {
          super(parent);
          this.attr = attr;
     }
 
     @Override
     protected Object openDialogBox(Control cellEditorWindow) {
          if (!attr.getAttribute().isMultiValueEnabled() &&
                    attr.getAttribute().getAttributeOptions() != null &&
                    attr.getAttribute().getAttributeOptions().size() > 0) {
               final AttributeOptionDialog dialog = new AttributeOptionDialog(cellEditorWindow.getShell(), 
                         attr.getValue(), attr.getAttribute());
               dialog.open();
               return dialog.getValue();
          }
          return onOpenDialogBox(cellEditorWindow);
     }
     
     protected abstract Object onOpenDialogBox(Control cellEditorWindow);
}

 

Then we can make three minor changes to each of the dialog cell editors in AttributeEditingSupport.java -- change the class they extend to AttributeDialogCellEditor, add 'attr' as an argument to super(), and rename the openDialogBox() method to onOpenDialogBox().

 

To finish up attribute editing, there are two other places in AttributeEditingSupport where we need to display our dialog:

 

     public static Window getEditorDialog(final AttributeValue attr, final Shell shell) {
          /* Begin new code */
          if (!attr.getAttribute().isMultiValueEnabled() &&
                         attr.getAttribute().getAttributeOptions() != null &&
                         attr.getAttribute().getAttributeOptions().size() > 0) {
               return new AttributeOptionDialog(shell, attr.getValue(), attr.getAttribute());
          }
          /* End new code */

 

And:

 

     protected CellEditor getCellEditor(final AttributeValue attr) {
          final Table table = this.attributesTableViewer.getSwtTable();
          CellEditor result = null;
          switch (attr.getAttributeType().getTypeId()) {
          case AttributeType.SHORT_TEXT_TYPE_ID:
               if (attr.getAttribute().isMultiValueEnabled()
                              /* Begin new code */
                              || (attr.getAttribute().getAttributeOptions() != null &&
                              attr.getAttribute().getAttributeOptions().size() > 0)
                              /* End new code */
                         ) {
                    result = new ShortTextCellEditor(table, attr);
                    break;
               }

 

The former change will show our dialog when the user presses the edit button, and the latter will show our dialog when a short text value's cell is clicked (which is normally directly editable).

 

For multi-valued short text values, we need to modify the addAction() and editAction() methods of the ShortTextMultiValueDialog. Attribute values will be entered using our new dialog, if the attribute has options:

 

     private void addAction() {
          String addedValue = null;
          if ( model.getAttribute().getAttributeOptions() != null &&
               model.getAttribute().getAttributeOptions().size() > 0 ) {
               final AttributeOptionDialog dialog = new AttributeOptionDialog(getShell(),
                         null, model.getAttribute());
               if ( dialog.open() != Window.OK ) {
                    return;
               }
               addedValue = (String)dialog.getValue();
               
               if (addedValue == null) {
                    return;
               }
          } else {
               final ShortTextDialog dialog = new ShortTextDialog(getShell(),
                         null, false);
               final int result = dialog.open();
               if (result != Window.OK) {
                    return;
               }
               addedValue = dialog.getValue();
          }
 
          if (addedValue == null) {
               return;
          }
 
          getShortTextValues().add(addedValue);
          setShortTextValues(getShortTextValues());
          refreshViewer();
          setValue(getModel().getStringValue());
 
     }
 
 
     private void editAction() {
          String editedValue = null;
          final String originalString = getSelectedValue();
          if ( model.getAttribute().getAttributeOptions() != null &&
               model.getAttribute().getAttributeOptions().size() > 0 ) {
               final AttributeOptionDialog dialog = new AttributeOptionDialog(getShell(),
                         originalString, model.getAttribute());
               if ( dialog.open() != Window.OK ) {
                    return;
               }
               editedValue = (String)dialog.getValue();
               
               if (editedValue == null) {
                    return;
               }
          } else {
               final ShortTextDialog dialog = new ShortTextDialog(getShell(),
                         originalString, true);
               final int result = dialog.open();
               if (result != Window.OK) {
                    return;
               }
               editedValue = dialog.getValue();
               
               if (editedValue == null) {
                    return;
               }
          }
 
          List<String> editedList = new ArrayList<String>();
          for (String element : getShortTextValues()) {
               if (element.equals(originalString)) {
                    editedList.add(editedValue);
               } else {
                    editedList.add(element);
               }
          }
 
          setShortTextValues(editedList);
 
          shortTextValueTableViewer.setInput(editedList.toArray());
          shortTextValueTableViewer.getSwtTableViewer().refresh();
          setValue(getModel().getStringValue());
 
     }
 

 

Creating the Options

 

Finally, the administrator needs to input the list of options for each particular attribute. We will make the necessary changes in a subclass of com.elasticpath.cmclient.catalog.dialogs.catalog.CatalogAttributesAddEditDialog:

 

public class AttributeOptionCatalogAttributesAddEditDialog extends CatalogAttributesAddEditDialog 
     implements SelectionListener, ISelectionChangedListener {
     private IEpTableViewer attributesTableViewer;
 
     private Button removeButton;
 
     private Button addButton;
 
     public CatalogAttributesWithOptionsDialog(final Shell parentShell,
          final Attribute attribute, final boolean isGlobal) {
          super(parentShell, attribute, isGlobal);
     }

 

To createEpDialogContent(), we will add a table to display the options:

     protected void createEpDialogContent(
               final IEpLayoutComposite dialogComposite) {
          super.createEpDialogContent(dialogComposite);
          
          final IEpLayoutData labelData = dialogComposite.createLayoutData(
                    IEpLayoutData.END, IEpLayoutData.FILL);
          final IEpLayoutData fieldData = dialogComposite.createLayoutData(
                    IEpLayoutData.FILL, IEpLayoutData.FILL, true, false);
 
          dialogComposite.addLabelBold(
                    "Restrict values to", //$NON-NLS-1$
                    labelData);
 
          dialogComposite.addLabel(
                    "", //$NON-NLS-1$
                    fieldData);
          
          IEpLayoutComposite allowedValuesComposite = dialogComposite.addGridLayoutComposite(2, false, 
                    dialogComposite.createLayoutData(IEpLayoutData.FILL, IEpLayoutData.FILL, true, false, 2, 1));
          attributesTableViewer = allowedValuesComposite.addTableViewer(false,
                    EpState.EDITABLE, 
                    allowedValuesComposite.createLayoutData(IEpLayoutData.END, IEpLayoutData.FILL, false, true));
          
          attributesTableViewer.addTableColumn( "Value", //$NON-NLS-1$
                    400).setLabelProvider(new ColumnLabelProvider() {
               public String getText(final Object element) {
                    final AttributeValue value = (AttributeValue) element;
                    return value.getStringValue();
               }
          });
          
          attributesTableViewer.setContentProvider(new IStructuredContentProvider() {
               public Object[] getElements(final Object inputElement) {
                    Attribute attributeInput = (Attribute)inputElement;
                    if (attributeInput.getAttributeOptions() != null) {
                         List<AttributeValueWithType> attributeOptions = new ArrayList<AttributeValueWithType>();
                         for( AttributeValue av : attributeInput.getAttributeOptions() ) {
                              attributeOptions.add((AttributeValueWithType)av);
                         }
                         Collections.sort(attributeOptions);
                         return attributeOptions.toArray();
                    }
                    return new Object [0];
               }
               public void dispose() {}
               public void inputChanged(Viewer arg0, Object arg1, Object arg2) {}
          });
          
          IEpLayoutComposite buttonsComposite = allowedValuesComposite.addGridLayoutComposite(1, false, null);
 
          // create add button
          final Image addImage = CoreImageRegistry
                    .getImage(CoreImageRegistry.IMAGE_ADD);
          addButton = buttonsComposite.addPushButton(
                    "Add value", addImage, //$NON-NLS-1$
                    EpState.EDITABLE, buttonsComposite.createLayoutData(
                              IEpLayoutData.FILL, IEpLayoutData.BEGINNING));
          addButton.addSelectionListener(this);
          
          // create remove button
          final Image removeImage = CoreImageRegistry
                    .getImage(CoreImageRegistry.IMAGE_REMOVE);
          removeButton = buttonsComposite.addPushButton(
                    "Remove value", removeImage, //$NON-NLS-1$
                    EpState.EDITABLE, buttonsComposite.createLayoutData(
                              IEpLayoutData.FILL, IEpLayoutData.BEGINNING));
          removeButton.addSelectionListener(this);
 
          attributesTableViewer.getSwtTableViewer().addSelectionChangedListener(this);
     }

 

We will initialize our new controls in populateControls():

     public void populateControls() {
          super.populateControls();
          
          addButton.setEnabled(true);
          removeButton.setEnabled(true);
          attributesTableViewer.setInput(getAttribute());
     }
     

 

And (finally!), we can handle the add/remove functionality, leveraging the existing attribute editing dialogs for entering our values:

 

     public void widgetDefaultSelected(SelectionEvent arg0) {
          // not used
     }
     
     private AttributeValue getSelectedValue() {
          return (AttributeValue) ((IStructuredSelection) attributesTableViewer.getSwtTableViewer()
                    .getSelection()).getFirstElement();
     }
 
     public void widgetSelected(SelectionEvent event) {
 
          if ( event.getSource() == addButton) {
               addValue();
          } else if ( event.getSource() == removeButton) {
               final AttributeValue attribute = getSelectedValue();
               if ( attribute != null ) {
                    removeValue(attribute);
               }
          }
          
     }
     
     private void addValue() {
          AttributeValue newOpt = Application.getInstance().getElasticPath().getBean(ContextIdNames.ATTRIBUTE_OPTION);
          newOpt.setAttribute(attribute);
          newOpt.setLocalizedAttributeKey(attribute.getKey());
          newOpt.setAttributeType(attribute.getAttributeType());
          Window dialog = getEditorDialog(newOpt, getShell());
          if ( dialog.open() == Window.OK ) {
               final IValueRetriever retriever = (IValueRetriever) dialog;
               if ( retriever.getValue() != null ) {
                    newOpt.setValue(retriever.getValue());
                    List<AttributeValue> attributeOptions = attribute.getAttributeOptions();
                    if ( attributeOptions == null ) {
                         attributeOptions = new ArrayList<AttributeValue>();
                    }
                    attributeOptions.add(newOpt);
                    attribute.setAttributeOptions(attributeOptions);
                    attributesTableViewer.getSwtTableViewer().refresh();
               }
          }
     }
     
     private void removeValue(AttributeValue attr) {
          List<AttributeValue> attributeOptions = attribute.getAttributeOptions();
          if ( attributeOptions != null ) {
               for( int index = 0; index < attributeOptions.size(); index++ ) {
                    if ( attributeOptions.get(index).getUidPk() == attr.getUidPk() ) {
                         attributeOptions.remove(index);
                         attribute.setAttributeOptions(attributeOptions);
                         attributesTableViewer.getSwtTableViewer().refresh();
                         break;
                    }
               }
          }
     }
 
     public void selectionChanged(SelectionChangedEvent event) {
          //not used
     }
     
     private Window getEditorDialog(final AttributeValue attr, final Shell shell) {
 
          Window dialog = null;
          switch (attr.getAttributeType().getTypeId()) {
          case AttributeType.BOOLEAN_TYPE_ID:
               dialog = new BooleanDialog(shell, attr.getValue());
               break;
          case AttributeType.DATE_TYPE_ID:
               dialog = new DateTimeDialog(shell, attr.getValue(),
                         IEpDateTimePicker.STYLE_DATE);
               break;
          case AttributeType.DATETIME_TYPE_ID:
               dialog = new DateTimeDialog(shell, attr.getValue(),
                         IEpDateTimePicker.STYLE_DATE
                                   | IEpDateTimePicker.STYLE_DATE_AND_TIME);
               break;
          case AttributeType.DECIMAL_TYPE_ID:
               dialog = new DecimalDialog(shell, attr.getValue());
               break;
          case AttributeType.INTEGER_TYPE_ID:
               dialog = new IntegerDialog(shell, attr.getValue());
               break;
          case AttributeType.LONG_TEXT_TYPE_ID:
               dialog = new LongTextDialog(shell, attr.getValue());
               break;
          case AttributeType.SHORT_TEXT_TYPE_ID:
               dialog = new ShortTextDialog(shell, attr.getValue(), true);
               break;
          case AttributeType.IMAGE_TYPE_ID:               
               dialog = AssetManager.createAssetManagerImagesDialog(shell);
               break;
          case AttributeType.FILE_TYPE_ID:               
               dialog = AssetManager.createAssetManagerFilesDialog(shell);
               break;
          default:
               // throw new RuntimeException("Unknown attribute type");
          }
          return dialog;
     }

 

Lastly we just replace all references of CatalogAttributesAddEditDialog with our new AttributeOptionCatalogAttributesAddEditDialog, and we're done! While not a trivial change, the existing infrastructure certainly made our job easier. Now we can restrict attributes to particular values if we need to, and not have to worry about mistypings or misspellings.

 

David Minor works for women's sportswear retailer Team Estrogen, an Elastic Path customer.