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

    Entity-, Form- or Conversation-Level validation

    dipas

      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

          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

            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.