This is w.r.t one of the most frequently used value object classes which is a part of Oracle IDM API – the oracle.iam.identity.usermgmt.vo.User class (you can always peek into the javadocs available here)
What I noticed was that this VO class does not override the equals and hashCode methods from the Object class… It uses them as is . . . (forgive me for posting this in case you knew it and found it too trivial ! 😉 )
What does this mean ?
- The User class uses the equals method implementation defined in the Object class itself which decides equality based on the whether or not the two references being compared point to the same instance. This essentially means that two User objects can only be deemed equal if they point to the same memory location on the heap i.e the below will always return false
User user1 = um.getDetails(UserManagerConstants.AttributeName.USER_LOGIN.getName(), "XELSYSADM", null); User user2 = um.getDetails(UserManagerConstants.AttributeName.USER_LOGIN.getName(), "XELSYSADM", null); System.out.println("Are the two Users the same? "+user1.equals(user2));
- The hashCode method in the Object class is a native implementation which traditionally works by converting the internal (heap) address of the object into an Integer. Because the User class just uses this implementation, its clear that two User objects will never have the same hash code even if they are conceptually the same (same User Login etc) i.e. the below code snippet will always return different values
User user1 = um.getDetails(UserManagerConstants.AttributeName.USER_LOGIN.getName(), "XELSYSADM", null); User user2 = um.getDetails(UserManagerConstants.AttributeName.USER_LOGIN.getName(), "XELSYSADM", null); System.out.println("Hash Code for User1 "+ user1.hashCode()); System.out.println("Hash Code for User2 "+ user2.hashCode());
You might be thinking . . what’s the big deal?!
Here is the thing – the above presented facts are not always going to cause an issue. For instance, one can argue that instead of using the equals method to compare two Users we can just compare them by their User Login or User Key (obtained by the getEntity() method) – this is absolutely correct
But there are certain scenarios (which are pretty obvious to be honest) where you will face problems – let’s look at one of them. . .
For instance, you have a HashMap wherein you have used instances of User objects as the key.
- Assume that you either build this HashMap or obtain this form another part of your API or some external client code
- If you have a User instance using which you would want to extract the values from the aforementioned Map – you won’t be able to do it. Try to execute the below code snippet in your OIM environment
public Map<User, Date> giveMeAMapOfUsers() { Map<User, Date> map = new HashMap<>(); try { UserManager um = oimclient.getService(UserManager.class); User user1 = um.getDetails( UserManagerConstants.AttributeName.USER_LOGIN.getName(), "XELSYSADM", null); Date dateOfCreation = new Date(); map.put(user1, dateOfCreation); } catch (Exception e) { e.printStackTrace(); } return map; } public void accessMapOfUsers(Map<User, Date> mapOfUsers) { try { UserManager um = oimclient.getService(UserManager.class); User user1 = um.getDetails( UserManagerConstants.AttributeName.USER_LOGIN.getName(), "XELSYSADM", null); Date dateOfCreation = mapOfUsers.get(user1); if (dateOfCreation != null) { System.out.println("My user was created on " + dateOfCreation.toString()); } else { System.out.println("??? null Date ???"); } } catch (Exception e) { e.printStackTrace(); } } TestObj obj = new TestObj(); obj.accessMapOfUsers(obj.giveMeAMapOfUsers());
You will notice that in spite of trying to search the HashMap with the same user (a different reference variable of course), the result is null – since the key (User instance) could not be found.
This result will not be surprising for folks who are aware of how Hash based collections work. A detailed explanation is not the point of this post 😉
This is happening because the User class is using the hashCode implementation from the Object class (as stated above). The HashMap search algorithm works on the basis of hash codes based on which it looks into its internal ‘buckets’. So, the point really is, one cannot use User object as keys of a HashMap.
Is there a way out? Of course !!! 🙂
For such scenarios, a possible work around is to create a sub class of the User class, override the equals and hashCode methods as per your requirements and utilize this.
public class MyUser extends User{ public MyUser(String entityId) { super(entityId); } @Override public boolean equals(Object aUser){ if(aUser == null){ return false; } User anotherUser = (User) aUser; if(anotherUser == this){ return true; } if(this.getEntityId().equals(anotherUser.getEntityId())){ return true; } return false; } @Override public int hashCode(){ return new Integer(super.getEntityId()).intValue(); } }
If you execute the above code snippets using this sub class, they will execute as expected i.e. obey the equals and hashCode contracts
User user1 = um.getDetails(UserManagerConstants.AttributeName.USER_LOGIN.getName(), "XELSYSADM", null); User user2 = um.getDetails(UserManagerConstants.AttributeName.USER_LOGIN.getName(), "XELSYSADM", null); MyUser myUser1 = new MyUser(user1.getEntityId()); MyUser myUser2 = new MyUser(user2.getEntityId()); System.out.println("Are the two Users the same? "+myUser1.equals(myUser2));
Will return true
public Map<User, Date> giveMeAMapOfUsers() { Map<User, Date> map = new HashMap<>(); try { UserManager um = oimclient.getService(UserManager.class); User user1 = um.getDetails( UserManagerConstants.AttributeName.USER_LOGIN.getName(), "XELSYSADM", null); Date dateOfCreation = new Date(); MyUser myUser1 = new MyUser(user1.getEntityId()); map.put(myUser1, dateOfCreation); } catch (Exception e) { e.printStackTrace(); } return map; } public void accessMapOfUsers(Map<User, Date> mapOfUsers) { try { UserManager um = oimclient.getService(UserManager.class); User user1 = um.getDetails( UserManagerConstants.AttributeName.USER_LOGIN.getName(), "XELSYSADM", null); MyUser myUser1 = new MyUser(user1.getEntityId()); Date dateOfCreation = mapOfUsers.get(myUser1); if (dateOfCreation != null) { System.out.println("My user was created on " + dateOfCreation.toString()); } else { System.out.println("??? null Date ???"); } } catch (Exception e) { e.printStackTrace(); } } TestBench obj = new TestBench(); obj.accessMapOfUsers(obj.giveMeAMapOfUsers());
Will return the java.util.Date object
There are many other instances in the OIM API classes which make our code a little too verbose at times
Few scenarios
- What if I need to find out users provisioned on a specific date using the SearchCriteria class which the search method employs – not straightforward. I had to use a custom date class wherein the date had to be formatted and was forced to check the provisioned date for each user . . . phew !
- What if I need to know whether an application instance is provisioned to a user? Yes – the obvious answer is to find out the provisioned instances of each user, iterate through them and match against your desired application instance name – is it ideal? How many object would you end up creating? How many queries would you fire when you use the ProvisioningService API ? Is the code performant ?? I doubt it….
- Wouldn’t it have been nice of the oracle.iam.provisioning,vo.Account class (and other related classes as well) would have implemented java.lang.Comparable ?? Get them as a list, use the contains method – job done!
- Have you noticed that the NotificationResolver interface is not available as a part of the client API i.e. oimclient.jar . . . Not everyone will know where its located (at least without wasting some time 😉 ). It’s buried in OIMServer.jar !
This was just a teaser folks 😉 That’s all for now. . .
Cheers!
Thank you for sharing your findings. Please fix your code snippets as they are bit corrupted: no “” symbols
LikeLike
Thanks for reading Philipp. Sorry about the snippets! WP messed it up. I’ll delegate that job to github via gists!
LikeLike