Additional Blogs by Members
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member
0 Kudos

Introduction

What is the most feature-rich UI control in WebDynpro framework?

Sure, IWDTable is very complex and provides a lot of functionality (ohhh, buddies, you have to see what you'll get with NW04s ;-). IWDBusinessGraphics even requires separate editor. IWDGeoMap is sooo complex that no one is able to make it work 😉

But what about IWDInputField? Huh? From the first glance it is trivial. But:

  • it supports data of almost any simple types (any numeric, boolean, string, date, time, timestamp);
  • it automatically adds date picker pop-over for date type;
  • it automatically provides enhanced value selector (EVS) for enumerated types;
  • It allows to add custom value help for almost any type with object value selector (OVS).

Here I will revisit the 4th option while requirement to provide a control that allows user both to type values directly and to select values from predefined list is very common.

The OVS support is quite complex topic for newbie, so complex that it deserves separate tutorial. However, complexity does not ends here.

Out-of-the-box support provided by OVS is rarely sufficient. The inherent problem with OVS is that we operate programmatically either with object identity or object key but have to display some user-friendly presentation of object. Even worse, if we allow user to type value directly (and we do allow otherwise control is read-only) we have to convert back this presentation to object identity / key.

First this problem was discussed in Events in EVS SDN WebDynpro forum thread. As a part of discussion Bertram Ganz came up with solution that uses calculated attributes for conversion between key <-> display values for EVS (see corresponding tutorial). Obviously, the very same technique may be applied to OVS as well.

Also this solution is quite elegant and completely workable, I personally found calculated attributes a bit inconvenient. The problem is that you have to create manually calculated attributes again and again whenever you apply the same OVS functionality. As a result internal logic of key <-> display value mapping is spread among several methods and reproduced in several components / controllers. The better way is to wrap the logic into reusable model class (to be exact, in getter / setter of corresponding attribute).

The second thing I wish to emphasize here is that you are not limited to OVS constructed over context node. Again, this way you forced to create context nodes for input parameters and output list every time you are applying OVS. It is acceptable for quick prototyping or one-time-only solutions. For reusable things you may jump to more robust approach with OVS by query (com.sap.tc.cmi.model.ICMIQuery). And you are not limited to Adaptive RFC Model or Enterprise Services Framework only. You may construct ICMIQuery yourself as well.

So, below I'll present set of utility classes that allow quickly implement "user picker" functionality via OVS.

Roll your own ICMQuery

Let us start with the most complex part - implementation of ICMIQuery.

package com.sap.sdn.samples.ume.beans; import java.util.Locale; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.sap.sdn.samples.cmi.CMIInfo; import com.sap.sdn.samples.cmi.CMIBean; import com.sap.security.api.ISearchAttribute; import com.sap.security.api.ISearchResult; import com.sap.security.api.IUser; import com.sap.security.api.IUserFactory; import com.sap.security.api.IUserSearchFilter; import com.sap.security.api.PrincipalIterator; import com.sap.security.api.UMException; import com.sap.security.api.UMFactory; import com.sap.tc.cmi.exception.CMIException; import com.sap.tc.cmi.metadata.ICMIModelClassInfo; import com.sap.tc.cmi.metadata.ICMIModelObjectCollectionInfo; import com.sap.tc.cmi.model.ICMIQuery; import com.sap.tc.webdynpro.basesrvc.util.StringUtil; public class SearchUsers extends CMIBean implements ICMIQuery { private Collection _result = null; private String _displayNameFilter = "*"; private String _uniqueNameFilter = "*"; private String _emailFilter = "*"; final private Comparator _resultComparator; SearchUsers() { this(null); } SearchUsers(final Comparator resultComparator) { _resultComparator = resultComparator; } public long countOf() { return -1; } public void invalidate() { _result = null; } public void execute() throws CMIException { try { final IUserFactory users = UMFactory.getUserFactory(); final IUserSearchFilter filter = users.getUserSearchFilter(); final String[] names = names( _displayNameFilter ); if ( !skipFilter(names[0]) ) filter.setFirstName( names[0], ISearchAttribute.LIKE_OPERATOR, false); if ( !skipFilter(names[1]) ) filter.setLastName( names[1], ISearchAttribute.LIKE_OPERATOR, false); if ( !skipFilter(_uniqueNameFilter) ) filter.setUniqueName( _uniqueNameFilter, ISearchAttribute.LIKE_OPERATOR, false ); if ( !skipFilter( _emailFilter ) ) filter.setEmail( _emailFilter, ISearchAttribute.LIKE_OPERATOR, false ); final ISearchResult sresult = users.searchUsers(filter); if ( sresult.getState() != ISearchResult.SEARCH_RESULT_OK ) throw new CMIException("Error searching users"); final ArrayList output = new ArrayList( sresult.size() ); for ( final Iterator i = new PrincipalIterator(sresult); i.hasNext(); ) output.add( new Result( (IUser)i.next() ) ); if ( null != _resultComparator ) Collections.sort ( output, new Comparator() { public int compare(final Object o1, final Object o2) { return _resultComparator.compare ( ((Result)o1).user(), ((Result)o2).user() ); } } ); _result = output; } catch (final UMException ex) { throw new CMIException(ex); } } public Object getInputParameter() { return this; } public Collection getResult() { return _result; } public boolean isDirty() { return null == _result; } public ICMIModelClassInfo associatedInputParameterInfo() { return associatedModelClassInfo(); } public ICMIModelClassInfo associatedModelClassInfo() { return INPUT_INFO; } public ICMIModelObjectCollectionInfo associatedResultInfo() { return RESULT_INFO; } public String getDisplayNameFilter() { return _displayNameFilter; } public void setDisplayNameFilter(final String value) { _displayNameFilter = StringUtil.isEmpty(value) ? "*" : value; } public String getUniqueNameFilter() { return _uniqueNameFilter; } public void setUniqueNameFilter(final String value) { _uniqueNameFilter = StringUtil.isEmpty(value) ? "*" : value; } public String getEmailFilter() { return _emailFilter; } public void setEmailFilter(final String value) { _emailFilter = StringUtil.isEmpty(value) ? "*" : value; } public Object getAttributeValue(final String name) { if ( "displayNameFilter".equals(name) ) return getDisplayNameFilter(); if ( "uniqueNameFilter".equals(name) ) return getUniqueNameFilter(); if ( "emailFilter".equals(name) ) return getEmailFilter(); return super.getAttributeValue(name); } public void setAttributeValue(final String name, final Object value) { if ( "displayNameFilter".equals(name) ) setDisplayNameFilter((String)value); else if ( "uniqueNameFilter".equals(name) ) setUniqueNameFilter((String)value); else if ( "emailFilter".equals(name) ) setEmailFilter((String)value); else super.setAttributeValue(name, value); } public static class Result extends CMIBean { final private IUser _user; Result(final IUser user) { _user = user; } public String getDisplayName() { return _user.getDisplayName(); } public String getEmail() { return _user.getEmail(); } public IUser user() { return _user; } public Object getAttributeValue(final String name) { if ( "email".equals(name) ) return getEmail(); if ( "displayName".equals(name) ) return getDisplayName(); return super.getAttributeValue(name); } public void setAttributeValue(final String name, final Object value) { if ( "email".equals(name) || "displayName".equals(name) ) throw new IllegalArgumentException ( "Property " + name + " is read-only" ); super.setAttributeValue(name, value); } /** * Code for *INFO objects and Regexp parsing skipped here */ }

What this class does? Sure, it implements ICMIQuery interface expected by OVS. The interface itself is not complex. Let us study it step-by-step:

  • Method getInputParameter() has to return reference to ICMIGenericModelClass that provides query input parameters. SearchUsers exposes input parameters directly so the method return reference to enclosing object. It's not unique approach - Adaptive RFC' *_Input classes do exactly the same. As a side effect, SearchUsers class has to implement ICMIGenericModelClass as well.
  • As far as query object holds input parameters itself (not via some nested model object) it provides the same results for both methods associatedModelClassInfo() and associatedInputParameterInfo().
  • The isDirty() method returns internal state of query - whether or not it was executed, and, therefore result is accessible.
  • The execute method do search itself. First we apply search filter, then execute UME search, next transform received principals into custom Result objects, and (finally and optionally) sort results.
  • getResult() method returns collection of users found (wrapped into SearchUsers.Result instabce) or null if query was invalidated / not yet executed.
  • invalidate() method just resets query state to non-valid and drops result collection.

Both SearchQuery and SearchQuery.Result classes implement ICMIGenericModelClass. Hence you may see (besides typed method to access input parameters in former or result attributes in later) generic attribute getter / setter methods (getAttributeValue(String attr) / setAttributeValue(String attr, Object value). The second part of ICMIGenericModelClass contract requires to return valid metadata. Here both classes implements assotiatedModelClassInfo() method.

It is extremely important that metadata provided by ICMIQuery is complete (field labels, column labels, external length of fields, type restrictions have to be defined). OVS has no other source of information to construct UI except model class info for input / result. So here I used set of simple dictionary types to describe metadata exactly. Also I changed CMIInfo class a bit since it introduction in my The quik brouwn fox jamps ovrr the laizy dog. Besides renaming classes, I swap property metadata container from java.util.TreeMap to java.util.LinkedHashMap. New implementation preserves order of attributes, i.e. they are guaranteed to be returned in insertion order.

Believe me or not the net result is complete ICMIQuery implementation. Ok, this implementation is sufficient for OVS functionality. To be 100% correct it is necessary to return result collection via getRelatedModelObjects(String relation) and to expose associatedModelInfo() form model class info. But we can safely ignore these requirements here.

Some details on UME search. Search is done by user logon id, e-mail and name. The latest filter is the most complex one. If we apply search mask directly to display name attribute we are unable to set any other attribute. This is documented UME limitation. Therefore we apply first name and last name attributes separately, assuming that users enters filter in one of the following formats: "Last_name, First_name" or "First_name Last_name". Error handling here is not very robust as well. For example, you may try to handle partially returned results (due to very large result set) as well.

IUserInfo and its implementation

UserInfo is just a regular JavaBean with methods to set / access underlying com.sap.security.api.IUser instance (used by developer code only) as well as getter / setter for "smart key" of user (bindable to IWDInputField). This is my replacement for calculated attribute. Getter method is trivial - it simply returns display name of wrapped IUser. Setter is more interesting. What I like to achieve is functionality, when for entered search mask bean automatically find one and only one user in most cases. So the logic hidden behind UserFinders.find(String mask) call is the following:

  1. Check if name looks like display name.
  2. If [1] is true, split mask up to 2 parts and try to find user by first / last name.
  3. If more than one user retuned by [2] try to find user first by e-mail, then by login till exactly one entry found or break if none found.
  4. If [1] is false, try to find exactly one user by login, e-mail, last name sequentially till exactly one entry found or break if none found

package com.sap.sdn.samples.ume.beans; import java.text.MessageFormat; import com.sap.security.api.IUser; import com.sap.tc.cmi.exception.CMIException; import com.sap.tc.webdynpro.basesrvc.util.StringUtil; import com.sap.tc.webdynpro.services.exceptions.WDNonFatalRuntimeException; public class UserInfo implements IUserInfo { private IUser _user; private String _description; public UserInfo() {} public UserInfo(final IUser user) { apply( user ); } public void apply(final IUser user) { _user = user; if ( null == _user ) { _description = null; return; } final String email = _user.getEmail(); if ( !StringUtil.isEmpty(email) ) { final String name = _user.getDisplayName(); if ( StringUtil.isEmpty( name ) ) _description = email; else _description = new MessageFormat("{0} '<'{1}'>'") .format( new Object[]{ name, email } ); } else _description = null; } public IUser user() { return _user; } public String getSmartKey() { return null == _user ? null : _user.getDisplayName(); } public void setSmartKey(final String value) { if ( StringUtil.isEmpty(value) ) { apply(null); return; } try { apply ( UserFinder.find( value ) ); } catch (final CMIException ex) { apply( null ); throw new WDNonFatalRuntimeException(ex); } } public String getDescription() { return _description; } }

IUserInfo serves as input for JavaBean model class importer (so we get JavaBean model with exactly one model class interface).

Attaching OVS helper

To make things even simpler for library user, I introduced UserOvsBinder utility class. It has several overloaded methods to enhance attribute with OVS support over SearchUsers query. An assumption that we are always binding to model node allows me to create shared com.sap.tc.webdynpro.progmodel.api.IWDOVSNotificationListener and completely hide this functionality (as far as OVS now relies on known signature of 2 JavaBeans).

package com.sap.sdn.samples.ume.beans; import java.text.Collator; import java.util.Comparator; import java.util.Iterator; import java.util.Locale; import com.sap.security.api.IUser; import com.sap.tc.webdynpro.progmodel.api.IWDAttributeInfo; import com.sap.tc.webdynpro.progmodel.api.IWDNodeElement; import com.sap.tc.webdynpro.progmodel.api.IWDNodeInfo; import com.sap.tc.webdynpro.progmodel.api.IWDOVSNotificationListener; import com.sap.tc.webdynpro.progmodel.api.WDValueServices; import com.sap.tc.webdynpro.services.sal.localization.api.WDResourceHandler; public class UserOvsBinder { private UserOvsBinder() {} public static void bind(final IWDNodeInfo node) { bind(node, "smartKey"); } public static void bind(final IWDNodeInfo node, final String attribute) { if ( null == node ) throw new IllegalArgumentException ( "Cannot bind OVS extension to null node" ); final IWDAttributeInfo attrInfo = node.getAttribute(attribute); if ( null == attrInfo ) throw new IllegalArgumentException ( "Unnable to find target attribute: " + attribute + ", node" + node ); bind( attrInfo ); } public static void bind(final IWDAttributeInfo attribute) { WDValueServices.addOVSExtension ( SearchUsers.class.getName(), new IWDAttributeInfo[]{ attribute }, new SearchUsers( new UserComparator() ), NOTIFICATION_LISTENER ); } final public static IWDOVSNotificationListener NOTIFICATION_LISTENER = new IWDOVSNotificationListener() { public void onQuery(final Object query) {} public void applyResult(final IWDNodeElement target, final Object result) { final SearchUsers.Result myResult = (SearchUsers.Result)result; final IUserInfo info = (IUserInfo)target.model(); info.apply( myResult.user() ); for ( final Iterator i = target.node().getNodeInfo().iterateAttributes(); i.hasNext(); ) { final IWDAttributeInfo attr = (IWDAttributeInfo)i.next(); if ( !attr.isReadOnly() ) target.changed( attr.getName() ); } } public void applyInputValues(final IWDNodeElement target, final Object query) { final SearchUsers myQuery = (SearchUsers)query; myQuery.invalidate(); final IUserInfo info = (IUserInfo)target.model(); final IUser current = info.user(); if ( null != current ) { myQuery.setDisplayNameFilter( current.getDisplayName() ); myQuery.setUniqueNameFilter( current.getUniqueName() ); myQuery.setEmailFilter( current.getEmail() ); } else { myQuery.setDisplayNameFilter( null ); myQuery.setUniqueNameFilter( null ); myQuery.setEmailFilter( null ); } } }; public static class UserComparator implements Comparator { final private Collator _collator; public UserComparator() { this( WDResourceHandler.getCurrentSessionLocale() ); } public UserComparator(final Locale locale) { _collator = Collator.getInstance(locale); } public int compare(final Object o1, final Object o2) { return compare( (IUser)o1, (IUser)o2 ); } public int compare(final IUser u1, final IUser u2) { final String dname1 = u1.getDisplayName(); final String dname2 = u2.getDisplayName(); return _collator.compare( dname1, dname2 ); } } }

Note the trick in applyResults(...) method. I manually set changed flag in node element attributes. Otherwise I get just wired behavior of IWDMessageManager. More on this at the end of post.

I have to admit, that I impose one restriction to the structure of context that uses this code. An OVS extension may be applied only to model nodes with IUserInfo model class. The workaround for value nodes is trivial: say, original node contains attribute for unique id of user. Then you have to add IUserInfo model sub-node with cardinality 1..1. Supply function for such sub-node is provided as well: UserNodeSupplier. The constructor of supply function accepts name of attribute that holds unique id of user. Changing sub-node element will force automatic synchronization of unique id in parent element. If you change unique id attribute in parent simply invalidate sub-node.

package com.sap.sdn.samples.ume.beans; import java.util.Collections; import com.sap.security.api.IUser; import com.sap.security.api.IUserFactory; import com.sap.security.api.UMException; import com.sap.security.api.UMFactory; import com.sap.tc.webdynpro.progmodel.api.IWDNode; import com.sap.tc.webdynpro.progmodel.api.IWDNodeCollectionSupplier; import com.sap.tc.webdynpro.progmodel.api.IWDNodeElement; import com.sap.tc.webdynpro.services.exceptions.WDNonFatalException; public class UserNodeSupplier implements IWDNodeCollectionSupplier { final private String _uidAttr; public UserNodeSupplier(final String uidAttr) { _uidAttr = uidAttr; } public void supplyElements(final IWDNode node, final IWDNodeElement parentElement) { final IUserInfo wrapee = new UserInfo(); final IUserInfo wraper = new IUserInfo() { public String getDescription() { return wrapee.getDescription(); } public String getSmartKey() { return wrapee.getSmartKey(); } public IUser user() { return wrapee.user(); } public void apply(final IUser user) { wrapee.apply( user ); syncUid(); } public void setSmartKey(final String value) { try { wrapee.setSmartKey( value ); } finally { syncUid(); } } private void syncUid() { final IUser user = user(); parentElement.setAttributeValue ( _uidAttr, null == user ? null : user.getUniqueID() ); parentElement.changed( _uidAttr ); } }; node.bind( Collections.singleton( wraper ) ); final String currentUID = (String)parentElement.getAttributeValue( _uidAttr ); if ( null != currentUID ) { try { final IUser currentUser = _Users.getUser( currentUID ); // Update wrapee, not wraper -- avoid changing attribute wrapee.apply( currentUser ); } catch (final UMException ex) { node.getContext() .getController().getComponent() .getMessageManager() .reportException ( new WDNonFatalException(ex), false ); } } } final private static IUserFactory _Users = UMFactory.getUserFactory(); }

Is it simple???

Yep, from implementer point of view it is not that simple. Especially, for first time. Believe me, third time you'll find this process just boring 😉

But how it looks from other side? Here is the code that attach SearchUsers OVS to several context nodes:

final IWDNode[] nodes = { wdContext.nodeAccountManager(), wdContext.nodeProjectCoordinator(), wdContext.nodeProjectManager() }; for (int i = 0; i < nodes.length; i++) UserOvsBinder.bind(nodes[i].getNodeInfo() );

Or, using 1..1 sub-node trick:

final IWDNodeInfo devUserInfo = wdContext .nodeDevelopers() .getNodeInfo() .getChild("DevUserInfo"); devUserInfo.setCollectionSupplier( new UserNodeSupplier("Guid") ); UserOvsBinder.bind( devUserInfo );

It's completely straightforward, isn't it?

The latest note. I found behavior of IWDMessageManager paired with OVS-enhanced attribute just crazy. Error messages get duplicated when OVS is invoked. When "Go" button is pressed they are vanished completely (note, however, that at this point we have not changed attribute value yet). Canceling OVS hides them till next UI control action invocation. Rrrrhhhhh...

image
Image 1. User picker at work

Download source code here (rename to *.zip after saving)

2 Comments