-
1. Re: Entity-, Form- or Conversation-Level validation
christian.bauer Jan 4, 2008 2:17 PM (in response to dipas)This wiki does exactly that by extending the Seam EntityHome framework stuff. With no extra configuration and the same amount of work for writing validation methods:
public String persist() { // Validate if (!isUniqueUsername() || !passwordAndControlNotNull() || !passwordMatchesRegex() || !passwordMatchesControl()) { return null; } }
And the validation and messaging is straightforward:
public boolean passwordMatchesControl() { if (password == null || passwordControl == null || !password.equals(passwordControl) ) { facesMessages.addToControlFromResourceBundleOrDefault( "passwordControl", FacesMessage.SEVERITY_ERROR, "lacewiki.msg.PasswordControlNoMatch", "The passwords don't match." ); return false; } return true; } public boolean isUniqueUsername() { User foundUser = userDAO.findUser(getInstance().getUsername(), false, false); if ( foundUser != null && foundUser != getInstance() ) { facesMessages.addToControlFromResourceBundleOrDefault( "username", FacesMessage.SEVERITY_ERROR, "lacewiki.msg.UsernameExists", "A user with that name already exists." ); return false; } return true; }
Throw in one or two convenience methods for message generation and that's it. I'm not sure why I would want some infrastructure doing this for me, I often have exceptions to what rules I want to trigger on which action.
I also want to call methods like isUniqueUsername() from an AJAX onblur event when the user edits the input field. And sometimes I need much more control, like pessimistic locking. I think the best place for all of this is in the regular action class.
-
2. Re: Entity-, Form- or Conversation-Level validation
dipas Jan 7, 2008 8:43 PM (in response to dipas)I tried the wiki example and some others too - but I had problems because I'm working with pageflow. The problem - in my tests - was that the exceptions where thrown in the
Invoke Application
-Phase - so I had to execute my store-methods through a pageflow-decision and a boolean outcome. But I don't like to produce outcomes only for controlling the pageflow in case of failure...So my current solution is as follows:
I annotate my Entity-Beans with Hibernate Validator
@Entity @org.hibernate.annotations.Table( appliesTo = "SysUser", indexes = @org.hibernate.annotations.Index( name = "IDX_SysUser_All", columnNames = {"Name", "Password", "Active", "ValidFrom", "ValidTo"}) ) public class SysUser implements Serializable { @Id @GeneratedValue @Column(name = "Id") private Long id; @Column( name = "Name", updatable = false) @org.hibernate.validator.Length( min = 2, max = 50 ) @org.hibernate.validator.NotNull private String name; @Column( name = "Password" ) @org.hibernate.validator.Length( min = 6, max = 70 ) @org.hibernate.validator.NotNull // TODO: Regexp für sicheres Passwort ? private String password; @Column( name = "ValidFrom" ) @Temporal(TemporalType.DATE) @org.hibernate.validator.NotNull private Date validFrom; @Column( name = "ValidTo" ) @Temporal(TemporalType.DATE) @org.hibernate.validator.Future private Date validTo; @Column(name = "RealName") @org.hibernate.validator.Length(max = 150) private String realName; .... and use AssertTrue / AssertFalse / Validate for Bean-Level checks @org.hibernate.validator.AssertTrue(message = "validTo~{validator.date.fromto.le}") public boolean isFromToValid() { return ValidationHelper.checkFromTo(validFrom, validTo); } ... more code
Then I annotate my SFSB:
@Stateful @Name("sysUserAdmin") @Scope(org.jboss.seam.ScopeType.CONVERSATION) public class SysUserAdminAction implements SysUserAdmin, Serializable { .... // OBSERVERS FOR VALIDATION -------------------------------------------------------------- @Observer("validateSysUserEdit") public boolean validateEdit() { if (password != null && !"".equals(password)) { sysUser.setPassword(password); } return ObjectLevelValidator.validate(this, sysUser); } // VALIDATE SYSUSER EDIT ------------------------------------------ @org.hibernate.validator.AssertTrue(message = "~SysUser_name~{SysUser.constraint.uniqueId}") private boolean checkUserHasUniqueName() { if (getNewData()) { if (sysUser.getName() == null) return false; return (sysUserDAO.findUsersByName(sysUser.getName()).size() == 0); } return true; } @org.hibernate.validator.AssertTrue(message = "password~{SysUser.constraint.passwordConfirm}") private boolean checkPasswordIsValid() { if (!getNewData() && "".equals(password) && "".equals(passwordConfirm)) return true; return (!"".equals(password) || !"".equals(passwordConfirm)) && password.equals(passwordConfirm); } ... }
now I have registered an Phase-Listener
public class LifeCycleListener implements PhaseListener { public void afterPhase(PhaseEvent evt) { if (PhaseId.UPDATE_MODEL_VALUES.equals(evt.getPhaseId())) { // send event only to the source object - not to all potential subscribers! org.jboss.seam.core.Events.instance().raiseEvent("validate" + getViewId(evt), evt.getSource()); } } public void beforePhase(PhaseEvent evt) { } public PhaseId getPhaseId() { return PhaseId.ANY_PHASE; } private String getViewId(PhaseEvent evt) { String viewId = evt.getFacesContext().getViewRoot().getViewId(); viewId = viewId.substring(viewId.lastIndexOf('/')+1, viewId.indexOf(".xhtml")); return viewId.substring(0, 1).toUpperCase() + viewId.substring(1); } }
and the ObjectLevelValidator does the rest of the job...
public class ObjectLevelValidator { // this is a utility class, ensure that no instances are created private ObjectLevelValidator() { super(); } /** * Validates all objects in the varArg and skips to RenderResponse if errors have been found * @param obj Objects to validate * @return true if errors found */ public static boolean validate(Object... obj) { return validate(true, obj); } /** * Validates all objects in the varArg and skips to RenderResponse if defined so. * @param skipToRenderResponseOnError if true, skip to RenderResponse if errors have been found * @param obj Objects to validate * @return true if errors are found */ public static boolean validate(boolean skipToRenderResponseOnError, Object... obj) { boolean valid = true; if (obj == null) return valid; FacesMessages msg = FacesMessages.instance(); ClassValidator classValidator; // validate all objects in the varArg for (Object o : obj) { if (o != null) { classValidator = Validators.instance().getValidator(o.getClass()); if (classValidator.hasValidationRules()) { InvalidValue[] badValues = classValidator.getInvalidValues(o); if (badValues.length > 0) { for (InvalidValue badValue : badValues) { String[] foundMessage; Boolean fullPropertyNameInMessage = badValue.getMessage().startsWith("~"); // split the message to see if there are AssertTrue / AssertFalse messages with an associated propertyName if (fullPropertyNameInMessage) { foundMessage = badValue.getMessage().substring(1).split("~"); } else { foundMessage = badValue.getMessage().split("~"); } if (foundMessage.length > 1) { String propertyName; if (fullPropertyNameInMessage) { // if the first sign in the message equals ~ then the full propertyName to attach the message is provided // e.g. message=~SysUser_name~{someMessageText} // used most often in Asserts on SFSB where you check properties of some Entity-Bean propertyName = foundMessage[0]; } else { // propertyName follows standard notation // e.g. message=name~{someMessageText} -> message attached to control // with id: EntityOrClassName_propertyName propertyName = badValue.getBeanClass().getSimpleName() + "_" + foundMessage[0]; } msg.addToControlFromResourceBundleOrDefault( propertyName, FacesMessage.SEVERITY_ERROR, "", foundMessage[1]); } else { // Standard notation: Error Messages are attached to Controls // with id: EntityOrClassName_propertyName msg.addToControlFromResourceBundleOrDefault( badValue.getBeanClass().getSimpleName() + "_" + badValue.getPropertyName(), FacesMessage.SEVERITY_ERROR, "", badValue.getMessage()); } } valid = false; } } } } // skip to RenderResponse if there are error(s) if (skipToRenderResponseOnError && !valid) { FacesContext.getCurrentInstance().renderResponse(); } return valid; } }
In my views (I use Facelet Composition Components) I have introduced the convention that each Input has an Id of format:
ClassName_fieldName
So when I check the name Field of the Entity-Bean inside of the SFSB I declare my Message like this:@org.hibernate.validator.AssertTrue(message = "~SysUser_name~{SysUser.constraint.uniqueId}")
Inside my Entity-Bean I declare the Message like this:
@org.hibernate.validator.AssertTrue(message = "validTo~{validator.date.fromto.le}")
I do not know yet what problems I will run into - but I think that I have not wasted very much time in this case. Because I still have the possibility to switch to your solution.
If you have any suggestion or you know that I will run into problems with this
approach, please give me a feedback.