I'm building a login form composite component. The page that uses it will pass an event handler that will validate the username and password. Usually (not using composite components) when we perform cross field validation via postValidate
, the event handler has to lookup the fields' components by name. It would be preferable for the validator not to do this, because these are inner details of the component that should be abstracted.
Any idea how I might get the converted values of the username and password fields in a postValidate
handler without knowing the inner details of the composite component?
Update:
The point of this is not to avoid looking up components by name at all, but to be able to cross-field validate the composite component's fields in a way that doesn't require the using page and/or bean to know the inner details of the component.
This can be done. In the following code, take particular note of the postValidate
event in the composite component and the postValidate
method in the backing component. Notice how it resolves the MethodExpression
attribute and invokes the passed-in method.
Here's the composite component:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:p="http://primefaces.org/ui">
<!-- Login form. -->
<cc:interface componentType="com.example.LoginForm">
<cc:attribute name="emailAddress" type="java.lang.String" required="true"/>
<cc:attribute name="rememberMe" type="java.lang.Boolean" required="true"/>
<cc:attribute name="checkCredentials"
method-signature="void checkCredentials(java.lang.String,java.lang.String,java.lang.String)"
shortDescription="Parameters are clientId, username and password. If credentials are invalid, attach a FacesMessage to the component specified by clientId."
required="true"/>
<cc:attribute name="actionListener" method-signature="void actionListener()" required="true"/>
<cc:attribute name="registerOutcome" type="java.lang.String" required="true"/>
<cc:attribute name="recoverPasswordOutcome" type="java.lang.String" required="true"/>
<cc:attribute name="headerTitle" type="java.lang.String" default="Sign In"/>
<cc:attribute name="emailAddressLabel" type="java.lang.String" default="Email address:"/>
<cc:attribute name="passwordLabel" type="java.lang.String" default="Password:"/>
<cc:attribute name="rememberMeLabel" type="java.lang.String" default="Stay signed in on this machine"/>
<cc:attribute name="loginLabel" type="java.lang.String" default="Sign In"/>
<cc:attribute name="recoverPasswordLabel" type="java.lang.String" default="Forgot password?"/>
<cc:attribute name="emailAddressRequiredMessage" type="java.lang.String" default="Email address required"/>
<cc:attribute name="passwordRequiredMessage" type="java.lang.String" default="Password required"/>
<cc:attribute name="registerLabel" type="java.lang.String" default="Register"/>
</cc:interface>
<cc:implementation>
<h:outputStylesheet library="components/example/login-form" name="style.css"/>
<div id="#{cc.clientId}">
<h:form id="form">
<f:event type="postValidate" listener="#{cc.postValidate}"/>
<div style="margin-top:10px;">
<p:panel header="#{cc.attrs.headerTitle}" styleClass="loginPanel">
<div class="login-form_errorContainer">
<p:messages rendered="#{facesContext.maximumSeverity.ordinal ge 2}"/>
</div>
<h:panelGrid columns="3">
<h:outputText styleClass="login-form_label" value="#{cc.attrs.emailAddressLabel}"/>
<h:panelGroup styleClass="login-form_cell">
<h:inputText id="emailAddress"
value="#{cc.attrs.emailAddress}"
required="true"
requiredMessage="#{cc.attrs.emailAddressRequiredMessage}"
styleClass="login-form_field"
immediate="true"/>
</h:panelGroup>
<h:panelGroup/>
<h:outputText styleClass="login-form_label" value="#{cc.attrs.passwordLabel}"/>
<h:panelGroup styleClass="login-form_cell">
<h:inputSecret id="password"
value="#{cc.attrs.password}"
required="true"
requiredMessage="#{cc.attrs.passwordRequiredMessage}"
styleClass="login-form_field"
immediate="true"/>
</h:panelGroup>
<h:link styleClass="login-form_link" value="#{cc.attrs.recoverPasswordLabel}" outcome="#{cc.attrs.recoverPasswordOutcome}"/>
<h:panelGroup/>
<p:selectBooleanCheckbox value="#{cc.attrs.rememberMe}" itemLabel="#{cc.attrs.rememberMeLabel}" immediate="true"/>
<h:panelGroup/>
<h:panelGroup/>
<h:panelGroup>
<p:commandButton id="submitForm" value="#{cc.attrs.loginLabel}" actionListener="#{cc.attrs.actionListener}" update="form"/>
<span class="login-form_or">or</span>
<h:link styleClass="login-form_link" value="#{cc.attrs.registerLabel}" outcome="#{cc.attrs.registerOutcome}"/>
</h:panelGroup>
<h:panelGroup/>
</h:panelGrid>
</p:panel>
</div>
</h:form>
</div>
</cc:implementation>
</html>
The backing component:
@FacesComponent("com.example.LoginForm")
public class LoginFormComponent extends UIInput implements NamingContainer
{
@Override
protected Object getConvertedValue(FacesContext context, Object newSubmittedValue) throws ConverterException
{
UIInput emailAddressComponent = (UIInput) findComponent(EMAIL_ADDRESS_ID);
UIInput passwordComponent = (UIInput) findComponent(PASSWORD_ID);
String emailAddress = (String) emailAddressComponent.getValue();
String password = (String) passwordComponent.getValue();
return new LoginFormValue(emailAddress, password);
}
public void postValidate(ComponentSystemEvent e) {
FacesContext ctx = getFacesContext();
// Don't validate credentials if the username and/or password fields are invalid.
if (!ctx.getMessageList(EMAIL_ADDRESS_ID).isEmpty() || !ctx.getMessageList(PASSWORD_ID).isEmpty())
{
return;
}
LoginFormValue value = (LoginFormValue) getConvertedValue(null, null);
MethodExpression checkCredentials = (MethodExpression) getAttributes().get(CHECK_CREDENTIALS_ATTRIBUTE_NAME);
checkCredentials.invoke(ctx.getELContext(), new Object[]{getClientId(), value.getEmailAddress(), value.getPassword()});
}
@Override
public String getFamily()
{
return "javax.faces.NamingContainer";
}
public static final String CHECK_CREDENTIALS_ATTRIBUTE_NAME = "checkCredentials";
public static final String EMAIL_ADDRESS_ID = "form:emailAddress";
public static final String PASSWORD_ID = "form:password";
}
The LoginFormValue
class for completeness:
public class LoginFormValue
{
public LoginFormValue(String emailAddress, String password)
{
this.emailAddress = emailAddress;
this.password = password;
}
public String getEmailAddress()
{
return emailAddress;
}
public String getPassword()
{
return password;
}
private String emailAddress;
private String password;
}
The page that uses the login form:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ex="http://java.sun.com/jsf/composite/components/example">
<h:head>
<title></title>
</h:head>
<h:body>
<ui:composition template="/WEB-INF/templates/myLayout.xhtml">
<ui:define name="windowTitle">Sign In</ui:define>
<ui:define name="body">
<ex:login-form emailAddress="#{loginBean.emailAddress}"
rememberMe="#{loginBean.rememberMe}"
checkCredentials="#{loginBean.checkCredentials}"
actionListener="#{loginBean.submit()}"
recoverPasswordOutcome="recover-password"
registerOutcome="signup"/>
</ui:define>
</ui:composition>
</h:body>
</html>
And finally, the page's backing bean:
@Named
@RequestScoped
public class LoginBean implements Serializable
{
public String getEmailAddress()
{
return emailAddress;
}
public void setEmailAddress(String emailAddress)
{
this.emailAddress = emailAddress;
}
public boolean isRememberMe()
{
return rememberMe;
}
public void setRememberMe(boolean rememberMe)
{
this.rememberMe = rememberMe;
}
/** Action listener for login-form. Called after validation passes. */
public void submit()
{
User user = userDao.findByEmailAddress(emailAddress);
userRequestBean.login(user.getUserId());
// Remember me
if (!rememberMe)
{
return;
}
// Handle rememberMe here (create a cookie, etc.)
}
/** Called by the backing component's postValidate event handler */
public void checkCredentials(String clientId, String emailAddress, String password)
{
if (!securityEjb.checkCredentials(emailAddress, password))
{
FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Incorrect email address/password", null);
FacesContext ctx = FacesContext.getCurrentInstance();
ctx.addMessage(clientId, message);
ctx.renderResponse();
}
}
private String emailAddress = "";
private boolean rememberMe = true;
@Inject
private UserRequestBean userRequestBean;
@EJB
private SecurityEjb securityEjb;
@EJB
private UserDao userDao;
@EJB
private LoginCookieDao loginCookieDao;
}
The f:event postValidate
approach doesn't leave you with a lot of options.
The option I prefer is doing the validations on the last component of the form, and then passing in the other components using binding and f:attribute.
For example
<h:inputText id="field1" binding="#{field1}" ... />
<h:inputText id="field2" validator="#{...}">
<f:attribute name="field1" value="#{field1}"/>
</h:inputText>
Then in your validator you can grab the other components off the UIInput:
UIComponent field1 = field2.getAttributes().get("field1")
I have used JsfWarn for a similar problem, I think it solves your problem in a much cleaner way.
Unlike JSF validators the WarningValidators are performed prior to render-response after the model has been updated the application has been invoked, so you could simply access your application for validation results.
@Named
public class BarWarningValidator implements WarningValidator{
@Inject
private MyValidationBean validationBean;
@Override
public void process(FacesContext context, UIInput component, ValidationResult validationResult) {
if(!validationBean.isBarValid()) {
validationResult.setFacesMessage(new FacesMessage(FacesMessage.SEVERITY_WARN, "FooBar", "This is a warning."));
}
}
}
And add the validator to the targeted field:
<h:outputLabel for="bar" value="Default warning:" />
<h:inputText id="bar">
<jw:warning validator="#{barWarningValidator}" />
<f:ajax event="change" render="..." />
</h:inputText>
<h:message for="bar" />