2 Replies Latest reply on Jan 7, 2008 8:43 PM by di pas

    Entity-, Form- or Conversation-Level validation

    di pas Newbie

      Hi


      Reading the Seam Manual I did not find anything about a way to validate entire Forms or even entire Conversations before storing them to the database - only property level validation with s:validate(All) are performed and only if the fields are not empty - in my research about this issue I found even that JSF does not provide a standard way to do this type of validation. But what I found was an article from Rick Hightower Object level validation with JSF and Spring. Based on his ideas I tried to find a solution to solve this realy standard problem for my forms.


      Is some interested or did I miss something in the manual?

        • 1. Re: Entity-, Form- or Conversation-Level validation
          Christian Bauer Master

          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
            di pas Newbie

            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.