Before delving into insides of this post answer yourself several questions.
You have answered at least some of questions with "Yes, it happens to me" then this post is definitely for you. If not: Who knows, probably you "will be back" later. Anyway, I'm strongly discouraging you from applying techniques mentioned below if no such problems exists in your current development. Do not introduce complexity just because you think some technique is cool. But "if": then:, well, full speed ahead ๐
First, some of techniques mentioned (validation / property constraints) rely on certain behavior of WebDynpro. I really have no clue whether or not it is standard. Probably, moderators will be quite peaceful to not delete the post right after reading this line or blame me for non-standard solutions. I guess functionality existing up to moment is very important, and if some compatibility issues arise it is possible to discuss them in comments.
Second, I've developed my code using NW04s or Paris (yes, I'm definitely a lucky dog ;). I've done all my best to not break compatibility with earlier versions. But if you have any problems back-porting it to SP11+ we may discuss issues in comments as well.
Third ("last but not least"), I will not try to describe how to program JavaBeans; related Java site has enough information as well as JavaDocs bundled with NetWeaver.
Imaging, that we have to develop component to edit team of employees. Say, we need to create editor for some project's staff. Initially editor is pre-initialized by certain team of users from external party like some sophisticated staff allocation engine (we simply emulate this). User may add / remove employees from the team as well as edit properties of existing ones (we left UME magic outside this sample). While editor provides a great flexibility to final user, it is enforces one important constraint: variation of total team salary should not exceed certain percent of initial team's salary. User should see immediate feedback on this constraint violation. Also when exiting editor we need to inform external component about delta-changes to team - editor has to provide exact information on:
Without any additional delay, let us start with implementation.
First, we will create (almost) regular JavaBeans for our use case. Here I will discuss final result, however in reality it is an outcome of several trials-n-refactorings.
Okay, we have to define our Employee JavaBean. It is just regular Java class with corresponding property accessor/mutator methods. By the way, if you are developing your JavaBeans from scratch I recommend the following path:
Creating JavaBeans is as natural as drinking cup of java at morning (along with spending hour or more reading email/rss or blogging instead of coding/bug-fixing ๐ Here I just emphasize several important things with sample model beans:
Here is one of mutator methods exposed (changing US SSN, social security number):
public void setSsn(final String value) { if ( isEmpty(value) ) throw new InvalidPropertyValueException("SSN may not be empty"); if ( !SSN.matcher(value).matches() ) throw new InvalidPropertyValueException("Invalid SSN format, should be like 123-45-6789"); final String prev = _ssn; _ssn = value; _pcs.firePropertyChange("ssn", prev, value); }
First, we check whether or not this number is not empty and satisfies SSN pattern (XXX-XX-XXXX, where X is any digit). If any of constraints fails we throw an exception. Next we notify any listener registered about property change. Pay attention that before firing notification we set property state to the supplied value (side note: java.beans.VetoableChangeListeners should be notified before actual object state is modified). Hint: avoid passing mutable object to listeners if the same object is assigned as field value - for the very same reasons as with accessor return value.
Now it is time to discuss our exception class. You may note from supplied sample project that it extends com.sap.tc.webdynpro.services.exceptions.WDNonFatalException. And this is extremely important point. Regular JavaBeans may declare checked property exceptions on their methods. JavaBeans designed for WebDynpro models may not. This is due to fact that WebDynpro generator fails to generate typed IWDNodeElement sub-interface for model class with "exceptional" mutators. You may ask why I choose exactly this class rather then generic java.lang.RuntimeException? For good reason, of course ๐
Our hand-written code is not the only part that invokes mutator method on model JavaBean. The other part is WebDynpro framework itself. During transporting user input from client to server WebDynpro assign changed (and only changed, btw) values to IWDNodeElement's attributes. As far as the node in question is model node, attribute setter merely delegates call to corresponding model class mutator method. Next, if this method throws generic unchecked exception (checked is declined by generator, if you remember), WebDynpro data container is not able to deal with it. Unless... Unless its type or its super-type is com.sap.tc.webdynpro.services.exceptions.WDNonFatalRuntimeException. Here WebDynpro shines in all colors and respect us for gracefully handling invalid user input ๐ Moreover, when transporting user input WebDynpro framework keeps track of attribute in question. And if our model class declines change with this exception, WebDynpro assigning automatically error to corresponding IWDNodeElement attribute as with IWDMessageManager.reportInvalidContextAttribute(...) method!!! Get your money for nothin', get your chicks for free... To sweet to be true, but that's it.
The rest of Employee property mutators resemble the paradigms of setSsn() method. So we forward our attention to TotalSalaryDelta JavaBean.
package com.sap.sdn.samples.pojo.beans.employee; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.math.BigDecimal; import java.util.Collection; import java.util.Iterator; import java.util.Set; import java.util.HashSet; public class TotalSalaryDelta { final private BigDecimal _validVariation; final private BigDecimal _initial; final private Set _registered = new HashSet(); private BigDecimal _delta = new BigDecimal(0); final private PropertyChangeListener _changeListener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { _delta = _delta .add( (BigDecimal)evt.getNewValue() ) .subtract( (BigDecimal)evt.getOldValue() ); } }; public TotalSalaryDelta(final Collection employees, double validVariation) { _validVariation = new BigDecimal(validVariation).movePointRight(2); BigDecimal total = new BigDecimal(0); for (final Iterator i = employees.iterator(); i.hasNext(); ) { final IEmployee employee = (IEmployee)i.next(); total = total.add( employee.getSalary() ); employee.addPropertyChangeListener( "salary", _changeListener ); _registered.add( employee ); } _initial = total; } public BigDecimal getInitial() { return _initial; } public BigDecimal getDelta() { return _delta; } public boolean getChangeValid() { return _delta .abs() .movePointRight(2) .divide( _initial, BigDecimal.ROUND_UP ) .compareTo( _validVariation ) < 0; } public void register(final IEmployee employee) { if ( _registered.contains(employee) ) return; _delta = _delta.add( employee.getSalary() ); employee.addPropertyChangeListener( "salary", _changeListener ); } public void unregister(final IEmployee employee) { if ( !_registered.remove(employee) ) return; _delta = _delta.subtract( employee.getSalary() ); employee.removePropertyChangeListener( _changeListener ); } }
From the first look it's quite a strange beast. However, let us analyze it from use case prospective. First it constructor accepts a collection of IEmployee objects and get cumulative value of salary to internal immutable field. This is an initial total salary our externally supplied team has. Next we register a property change listener on IEmployee to track changes of salary property. As far as user is allowed to add/remove employees from team, bean also provides methods to register/deregister employees to track.
Someone may notice that I prefer to declare java.beans.PropertyChangeListener as internal variable rather then implement it by bean itself. From my point of view this approach lead to code exactness - listening for property changes is an implementation details of bean rather then part of it external interface. Keep this rule in mind when designing your own classes.
Finally, TotalSalaryDelta expose 3 read-only properties to access initial value of total salary, total salary delta and validity of change.
Now we are ready to import our beans into WebDynpro model.
If you are using SP11+ then here are some important hints from my pre-NW04s experience regarding model import. JavaBean Model Import wizard works properly only for beans packed into used public part (actually, it seems that wizard simply relies too heavily on JAR archive & manifest declaration). So, all JavaBeans should be placed into separate DC, added to public part "Java classes / assembly". Second, JavaBean model importer works properly only for JavaBeans these have declaration in manifest file from JavaBeans archive, see WebDynpro forum post Re: WebDynpro Using JavaBean Model ->Please Help by Anilkumar Vippagunta. So do not forget to create manifest file with necessary entries. Also the complete WebDynpro Using JavaBean Model ->Please Help is very enlighten, and I strongly recommend to read this discussion (as a bonus you will get some nifty utility for DB->JavaBeans-for-model-import generation).
Okay, so far so good. So if you are on SP11 add used DC with JavaBeans, if you are "in Paris" simply rebuild project. Now we are ready to launch JavaBeans model importer.
First step of wizard is trivial (defining lookup / output paths). At the second step we have to select 2 our JavaBeans. Enable option to automatically add super classes and interfaces. Right after clicking "Add" we got 5 entries in result list. 3 of them are actually necessary - IEmployee, Employee and TotalSalaryDelta. All the rest are just implementation helper classes and interfaces and hence may be removed. Go ahead to third step.
Here we face one unpleasant fact - wizard treats gender property as a relation. This is due to fact that actual property type (Gender) is not from "standard" types set (note, however, that both java.sql.Date and java.math.BigDecimal are welcome here). Don't panic, simply uncheck gender from list of relations and click finish. Wizard completes with report of successful model generation.
To start, expand generated model in WebDynpro tree. It contains one interface IEmployee, class Employee where all properties are inherited from interface and class TotalSalaryDelta. Open IEmployee interface and add gender property we had to skip at generation phase. Please note that model properties should start with lowercase letter due to WebDynpro name conversion rules.
Continue with customizing other properties. For this we create sub-types in Local Dictionary for built-in data types:
After creating necessary types (re-)open model class editor for IEmployee and change default types of corresponding property (right-click on property, select "Edit", select type from local dictionary).
Using custom dictionary types pays off when you start to configure WebDynpro components view(s). You may by-skip assigning text to labels, column align, column header texts, tooltips, and length of input fields - all the necessary data will be taken from custom type definition. By the way, length of input field is quite interesting in this regard; here is WebDynpro algorithm to choose this value (from higher priority to lower):
Component itself is a trivial Master (table) -> Details (form) editor for team of employees. All the interesting work happens in component controller, so let us discuss its implementation.
public void wdDoInit() { //@@begin wdDoInit() final Collection initial = new ArrayList(5); initial.add( newEmployee("Bob", "Smith", IEmployee.MALE, dt(1962, 10, 8), 74000) ); ... initial.add( newEmployee("Huw", "Thompson", IEmployee.MALE, dt(1956, 5, 18), 90000) ); _totalSalaryDelta = new TotalSalaryDelta(initial, 0.25); _employeeListDelta = new EmployeeListDelta(initial); wdContext.nodeEmployees().bind( initial ); //@@end }
First, wdDoInit start with initialization of fake data. In real-life scenario this data would came from external source or embedding component. After initializing initial team members, we are setting up necessary observers. First one was discussed above - it's an instance of TotalSalaryDelta. The second one is EmployeeListDelta for tracking Insert / Update / Delete changes of employees list:
package com.sap.sdn.samples.pojo.beans.employee; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Collection; import java.util.Iterator; import java.util.Set; import java.util.HashSet; import java.util.Collections; public class EmployeeListDelta { final private Set _original = new HashSet(); final private Set _inserted = new HashSet(); final private Set _updated = new HashSet(); final private Set _deleted = new HashSet(); final private PropertyChangeListener _employeeUpdate = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { final IEmployee empl = (IEmployee)evt.getSource(); if ( _original.contains(empl) ) { _updated.add(empl); empl.removePropertyChangeListener( _employeeUpdate ); } else throw new IllegalStateException("Update of inserted/deleted employee"); } }; public EmployeeListDelta(final Collection original) { _original.addAll( original ); for (final Iterator i = _original.iterator(); i.hasNext(); ) { final IEmployee empl = (IEmployee)i.next(); empl.addPropertyChangeListener( _employeeUpdate ); } } public void trackInsert(final IEmployee employee) { if ( _original.contains(employee) ) throw new IllegalArgumentException ( "Employee "" + employee + "" exists in original list" ); else { if ( !_inserted.add( employee ) ) throw new IllegalArgumentException ( "Attempt to insert employee "" + employee + "" twice" ); } } public void trackRemove(final IEmployee employee) { if ( _original.contains(employee) ) { if ( !_deleted.add( employee ) ) throw new IllegalArgumentException ( "Attempt to delete employee "" + employee + "" twice" ); employee.removePropertyChangeListener( _employeeUpdate ); } else { if ( !_inserted.remove( employee ) ) throw new IllegalArgumentException ( "Employee "" + employee + "" is not maintained" ); } _updated.remove( employee ); } public Set inserted() { return Collections.unmodifiableSet(_inserted); } public Set updated() { return Collections.unmodifiableSet(_updated); } public Set deleted() { return Collections.unmodifiableSet(_deleted); } public boolean isModified() { return !( _inserted.isEmpty() && _updated.isEmpty() && _deleted.isEmpty() ); } }
The next pair of methods actually add / remove employees to list as well as notify observers about list changes:
public void addEmployee( ) { //@@begin addEmployee() final Employee employee = new Employee(); _totalSalaryDelta.register( employee ); _employeeListDelta.trackInsert( employee ); final IPublicEmployeeDetails.IEmployeesNode nEmployees = wdContext.nodeEmployees(); final IPublicEmployeeDetails.IEmployeesElement elEmployee = nEmployees.createEmployeesElement( employee ); final int idx = nEmployees.getLeadSelection(); nEmployees.addElement( idx, elEmployee ); nEmployees.setLeadSelection( idx ); //@@end } public void removeEmployees( boolean isAll ) { //@@begin removeEmployees() final IPublicEmployeeDetails.IEmployeesNode nEmployees = wdContext.nodeEmployees(); final int lead = nEmployees.getLeadSelection(); for (int i = nEmployees.size() - 1; i >= 0; i--) { if ( isAll || i == lead || nEmployees.isMultiSelected(i) ) { final IPublicEmployeeDetails.IEmployeesElement elEmployee = nEmployees.getEmployeesElementAt(i); _totalSalaryDelta.unregister( elEmployee.modelObject() ); _employeeListDelta.trackRemove( elEmployee.modelObject() ); nEmployees.removeElement( elEmployee ); } } //@@end }
The latest interesting method here is save(). First, it consults EmployeeListDelta whether any change what applied at all. If no, it reports warning and returns, otherwise it provides statistica by every specific type of change (insertion, modification, deletion):
public void save( ) { //@@begin save() if ( !_employeeListDelta.isModified() ) { wdComponentAPI.getMessageManager().reportWarning("Employee list was not modified"); return; } showEmployeeDelta( "Inserted employees:", _employeeListDelta.inserted()); showEmployeeDelta( "Updated employees:", _employeeListDelta.updated()); showEmployeeDelta( "Deleted employees:", _employeeListDelta.deleted()); //@@end } private void showEmployeeDelta(final String type, final Collection c) { final IWDMessageManager reporter = wdComponentAPI.getMessageManager(); reporter.reportSuccess(type); for (final Iterator i = c.iterator(); i.hasNext(); ) reporter.reportSuccess("" + i.next() ); }
Models provide great benefits when you are working with WebDynpro context. You may share same model class object between 2 non-mapped nodes and still has data changes synchronized. You may track per-attribute updates of your data as user enters it. You may add validation rules of arbitrary complexity to properties (far beyond basic validation provided by simple types). Also you may achieve productivity boost due to reuse of objects' design - be it either creating context nodes or co-locating read-only accessors for calculated data in one place instead of replicating it in several controllers.
However, there are certain problems with WebDynpro JavaBean models. First, validation constraints require usage of WebDynpro-specific exception classes. Second, to benefit from rich WebDynpro data dictionary types you are sometimes forced to reference classes created by generator (and sometimes such dependency is very undesirable). Hence I put words "almost plain" in title of this post. Sure, it is possible to use WebDynpro JavaBeans model directly over EJB DTO (data transfer objects), but "advance" features described above require introduction of yet another level of indirection in form of WD-specific adapters over original DTO.
Oh, and I have to mention several things with my own sample code:
Good luck in your experiments,
VS
You may download sample project file here