Hibernate, EMF objects and eclipse databinding – ensure you have an equals method

Ran into a few problems the other day around creating UI forms with EMF that is backed by Hibernate and Teneo. The jface databinding snippets almost always use strings as the model object, however, most model objects have embedded objects in them that then, down the road, relate back to user-readable strings.  Hence the snippets don’t always show some nuances around the model objects.

Its important to realize that some of the jface databinding support classes, such as those producing object-to-label maps (which is why there are always IObservableMaps being used for everything are good in the sense that you can create a map between domain objects and the strings that should be displayed. These can be created automatically for you using jface databinding or you can create the maps directly yourself (no one really does that though). The real issues is to realize that many of these object-to-label maps that are created automatically use a backing map (hashtable) that hashes the object to provide the key. The key of course is your domain object of some sort. Well, for strings directly entered into your application, the strings all map to the same string and hence the same object at least form an equals() perspective. Hence, the map you create with the jface databinding classes always find the same object as the key and returns the string.

But with EMF, Hibernate and teneo in the background (actually just with any complex object that does not use a static list of objects or simple string values), for objects that are generated from a repository, those objects may be proxied or have a different notion of equals in them.  So while a program like the following modified snippet may work fine and make you fell confident that everything is going well:

   1: /*******************************************************************************
   2:  * Copyright (c) 2006, 2009 The Pampered Chef, Inc. and others.
   3:  * All rights reserved. This program and the accompanying materials
   4:  * are made available under the terms of the Eclipse Public License v1.0
   5:  * which accompanies this distribution, and is available at
   6:  * http://www.eclipse.org/legal/epl-v10.html
   7:  *
   8:  * Contributors:
   9:  *     The Pampered Chef, Inc. - initial API and implementation
  10:  *     Brad Reynolds - bug 116920
  11:  *     Matthew Hall - bug 260329
  12:  ******************************************************************************/
  13:  
  14: package org.eclipse.jface.examples.databinding.snippets;
  15:  
  16: import java.beans.PropertyChangeListener;
  17: import java.beans.PropertyChangeSupport;
  18: import java.util.ArrayList;
  19: import java.util.HashSet;
  20:  
  21: import org.eclipse.core.databinding.DataBindingContext;
  22: import org.eclipse.core.databinding.beans.BeanProperties;
  23: import org.eclipse.core.databinding.beans.BeansObservables;
  24: import org.eclipse.core.databinding.beans.PojoProperties;
  25: import org.eclipse.core.databinding.observable.Observables;
  26: import org.eclipse.core.databinding.observable.Realm;
  27: import org.eclipse.core.databinding.observable.list.WritableList;
  28: import org.eclipse.core.databinding.observable.map.IObservableMap;
  29: import org.eclipse.core.databinding.observable.value.IObservableValue;
  30: import org.eclipse.jface.databinding.swt.SWTObservables;
  31: import org.eclipse.jface.databinding.viewers.ObservableMapLabelProvider;
  32: import org.eclipse.jface.databinding.viewers.ViewerProperties;
  33: import org.eclipse.jface.databinding.viewers.ViewerSupport;
  34: import org.eclipse.jface.databinding.viewers.ViewersObservables;
  35: import org.eclipse.jface.layout.GridLayoutFactory;
  36: import org.eclipse.jface.viewers.ArrayContentProvider;
  37: import org.eclipse.jface.viewers.ComboViewer;
  38: import org.eclipse.jface.viewers.ListViewer;
  39: import org.eclipse.swt.SWT;
  40: import org.eclipse.swt.widgets.Combo;
  41: import org.eclipse.swt.widgets.Display;
  42: import org.eclipse.swt.widgets.List;
  43: import org.eclipse.swt.widgets.Shell;
  44: import org.eclipse.swt.widgets.Text;
  45:  
  46: /**
  47:  * Demonstrates nested selection.<br>
  48:  * At the first level, user may select a person.<br>
  49:  * At the second level, user may select a city to associate with the selected<br>
  50:  * person or edit the person's name.
  51:  */
  52: public class Snippet001NestedSelectionWithComboNoStrings {
  53:     public static void main(String[] args) {
  54:         Display display = new Display();
  55:         final ViewModel viewModel = new ViewModel();
  56:         Realm.runWithDefault(SWTObservables.getRealm(display), new Runnable() {
  57:             public void run() {
  58:                 Shell shell = new View(viewModel).createShell();
  59:                 Display display = Display.getCurrent();
  60:                 while (!shell.isDisposed()) {
  61:                     if (!display.readAndDispatch()) {
  62:                         display.sleep();
  63:                     }
  64:                 }
  65:             }
  66:         });
  67:     }
  68:  
  69:     // Minimal JavaBeans support
  70:     public static abstract class AbstractModelObject {
  71:         private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(
  72:                 this);
  73:  
  74:         public void addPropertyChangeListener(PropertyChangeListener listener) {
  75:             propertyChangeSupport.addPropertyChangeListener(listener);
  76:         }
  77:  
  78:         public void addPropertyChangeListener(String propertyName,
  79:                 PropertyChangeListener listener) {
  80:             propertyChangeSupport.addPropertyChangeListener(propertyName,
  81:                     listener);
  82:         }
  83:  
  84:         public void removePropertyChangeListener(PropertyChangeListener listener) {
  85:             propertyChangeSupport.removePropertyChangeListener(listener);
  86:         }
  87:  
  88:         public void removePropertyChangeListener(String propertyName,
  89:                 PropertyChangeListener listener) {
  90:             propertyChangeSupport.removePropertyChangeListener(propertyName,
  91:                     listener);
  92:         }
  93:  
  94:         protected void firePropertyChange(String propertyName, Object oldValue,
  95:                 Object newValue) {
  96:             propertyChangeSupport.firePropertyChange(propertyName, oldValue,
  97:                     newValue);
  98:         }
  99:     }
 100:  
 101:     // The data model class. This is normally a persistent class of some sort.
 102:     // 
 103:     // This example implements full JavaBeans bound properties so that changes
 104:     // to instances of this class will automatically be propogated to the UI.
 105:     public static class Person extends AbstractModelObject {
 106:         // Constructor
 107:         public Person(String name, City city) {
 108:             this.name = name;
 109:             this.city = city;
 110:         }
 111:  
 112:         // Some JavaBean bound properties...
 113:         String name;
 114:  
 115:         City city;
 116:  
 117:         public String getName() {
 118:             return name;
 119:         }
 120:  
 121:         public void setName(String name) {
 122:             String oldValue = this.name;
 123:             this.name = name;
 124:             firePropertyChange("name", oldValue, name);
 125:         }
 126:  
 127:         public City getCity() {
 128:             return city;
 129:         }
 130:  
 131:         public void setCity(City city) {
 132:             City oldValue = this.city;
 133:             this.city = city;
 134:             firePropertyChange("city", oldValue, city);
 135:         }
 136:     }
 137:  
 138:     // The View's model--the root of our GUI's Model graph
 139:     //
 140:     // Typically each View class has a corresponding ViewModel class.
 141:     // The ViewModel is responsible for getting the objects to edit from the
 142:     // DAO. Since this snippet doesn't have any persistent objects to
 143:     // retrieve, this ViewModel just instantiates some objects to edit.
 144:     // 
 145:     // This ViewModel also implements JavaBean bound properties.
 146:     static class ViewModel extends AbstractModelObject {
 147:         // The model to bind
 148:         private ArrayList people = new ArrayList();
 149:         {
 150:             people.add(new Person("Wile E. Coyote", City.from("Tucson")));
 151:             people.add(new Person("Road Runner", City.from("Lost Horse")));
 152:             people.add(new Person("Bugs Bunny", City.from("Forrest")));
 153:         }
 154:  
 155:         // Choice of cities for the Combo
 156:         private ArrayList<City> cities = new ArrayList<City>();
 157:         {
 158:             cities.add(City.from("Tucson"));
 159:             cities.add(City.from("AcmeTown"));
 160:             cities.add(City.from("Lost Horse"));
 161:             cities.add(City.from("Forrest"));
 162:             cities.add(City.from("Lost Mine"));
 163:         }
 164:  
 165:         public ArrayList getPeople() {
 166:             return people;
 167:         }
 168:  
 169:         public ArrayList<City> getCities() {
 170:             System.out.println("Accessing cities list");
 171:             return cities;
 172:         }
 173:     }
 174:  
 175:     static class City {
 176:         private String name;
 177:  
 178:         public void setName(String name) {
 179:             this.name = name;
 180:         }
 181:  
 182:         public String getName() {
 183:             return this.name;
 184:         }
 185:  
 186:         public City(String name) {
 187:             setName(name);
 188:         }
 189:  
 190:         public static City from(String name) {
 191:             return new City(name);
 192:         }
 193:  
 194:         /*
 195:          * (non-Javadoc)
 196:          * 
 197:          * @see java.lang.Object#equals(java.lang.Object)
 198:          */
 199:         @Override
 200:         public boolean equals(Object obj) {
 201:             if (obj == this)
 202:                 return true;
 203:             if (obj instanceof City) {
 204:                 if (this.getName().equals(((City) obj).getName()))
 205:                     return true;
 206:             }
 207:             return super.equals(obj);
 208:         }
 209:  
 210:         /*
 211:          * (non-Javadoc)
 212:          * 
 213:          * @see java.lang.Object#hashCode()
 214:          */
 215:         @Override
 216:         public int hashCode() {
 217:             return name.hashCode();
 218:         }
 219:     }
 220:  
 221:     // The GUI view
 222:     static class View {
 223:         private ViewModel viewModel;
 224:  
 225:         public View(ViewModel viewModel) {
 226:             this.viewModel = viewModel;
 227:         }
 228:  
 229:         public Shell createShell() {
 230:             // Build a UI
 231:             Shell shell = new Shell(Display.getCurrent());
 232:             // Realm realm = SWTObservables.getRealm(shell.getDisplay());
 233:  
 234:             List peopleList = new List(shell, SWT.BORDER);
 235:             Text name = new Text(shell, SWT.BORDER);
 236:             Combo city = new Combo(shell, SWT.BORDER | SWT.READ_ONLY);
 237:  
 238:             ListViewer peopleListViewer = new ListViewer(peopleList);
 239:             IObservableMap attributeMap = BeansObservables.observeMap(
 240:                     Observables.staticObservableSet(new HashSet(viewModel
 241:                             .getPeople())), Person.class, "name");
 242:             peopleListViewer.setLabelProvider(new ObservableMapLabelProvider(
 243:                     attributeMap));
 244:             peopleListViewer.setContentProvider(new ArrayContentProvider());
 245:             peopleListViewer.setInput(viewModel.getPeople());
 246:  
 247:             DataBindingContext dbc = new DataBindingContext();
 248:             IObservableValue selectedPerson = ViewersObservables
 249:                     .observeSingleSelection(peopleListViewer);
 250:             dbc.bindValue(SWTObservables.observeText(name, SWT.Modify),
 251:                     BeansObservables.observeDetailValue(selectedPerson, "name",
 252:                             String.class));
 253:  
 254:             ComboViewer cityViewer = new ComboViewer(city);
 255:             ViewerSupport.bind(cityViewer, new WritableList(viewModel
 256:                     .getCities(), City.class), PojoProperties.value("name"));
 257:             dbc.bindValue(ViewerProperties.singleSelection()
 258:                     .observe(cityViewer), BeanProperties.value("city",
 259:                     City.class).observeDetail(selectedPerson));
 260:  
 261:             GridLayoutFactory.swtDefaults().applyTo(shell);
 262:             // Open and return the Shell
 263:             shell.pack();
 264:             shell.open();
 265:             return shell;
 266:         }
 267:     }
 268:  
 269: }

This may not actually work correctly if you use EMF objects until you define equals() (and ideally hashcode() of course). The key code area is the following code which loosely translate into “bind the current selection in the combo viewer (which is typically a combo in database applications that wish to show a relationship to another object through a key) and have that stay in sync with property on the current selection object in the master viewer (typically a table) , allowing any of the values that are valid values from the entire list of cities".”



   1: ComboViewer cityViewer = new ComboViewer(city);
   2:             ViewerSupport.bind(cityViewer, new WritableList(viewModel
   3:                     .getCities(), City.class), PojoProperties.value("name"));
   4:             dbc.bindValue(ViewerProperties.singleSelection()
   5:                     .observe(cityViewer), BeanProperties.value("city",
   6:                     City.class).observeDetail(selectedPerson));


This is the mostly classic code of producing a ocmbo picker that tracks the object used in the domain model with a contained object, such is the City object. ViewerSupport is actually creating the observabel map behind the scenes:



   1: public static void bind(StructuredViewer viewer, IObservableList input,
   2:             IValueProperty[] labelProperties) {
   3:         ObservableListContentProvider contentProvider = new ObservableListContentProvider();
   4:         viewer.setContentProvider(contentProvider);
   5:         viewer.setLabelProvider(new ObservableMapLabelProvider(Properties
   6:                 .observeEach(contentProvider.getKnownElements(),
   7:                         labelProperties)));
   8:         viewer.setInput(input);
   9:     }


This is the snipped from eclipse’s ViewerSupport.  You can see that it creates an observable map label provider. This is okay and useful for simple labels that are string properties directly off the model. If you need a more clever string, or something formatted differently, you have to produce your own ColumnLabelProvider and add the label provider directly yourself in code. But this works for standard properties that return user-readable strings.  For other ways to create the observable map label provider see this blog from TomS: http://tomsondev.bestsolution.at/2009/06/27/galileo-emf-databinding-part-5/ .


If the input is a hibernate persistent array, the objects may be proxied. That’s okay in general, but you have to make sure that these objects understand how to hash and understand their own sense of equality.  Using the above ViewerSupport code with a hibernate list will frustrate the combo because when it goes to find the object to use as the key to obtain the (mapped) user display string, the object may be proxied. The default equals returns false for comparing the object that is used to originally create the list and the object in your “master selection” that is the embedded domain object within your master selection. Those two objects will not match and your combo will not update its display with the proper domain object (such as City in the snipped above, although the snipped above works because it does not have this problem).  The trick is to define equals() and hashcode() for your EMF based domain object. Then the example works.


The moral is that when the jface databinding framework uses maps to link domain objects to labels or images, ensure that you know when your domain object will return equals in a way that makes sense for application domain.


All of the snippets are located at this link http://wiki.eclipse.org/JFace_Data_Binding/Snippets and have to be downloaded from SVN here: dev.eclipse.org and project org.eclipes.jface.examples.databinding.

Comments

Popular posts from this blog

quick note on scala.js, react hooks, monix, auth

zio environment and modules pattern: zio, scala.js, react, query management

user experience, scala.js, cats-effect, IO