diff --git a/functional-test/pom.xml b/functional-test/pom.xml index 2ac4817c66..596304e553 100644 --- a/functional-test/pom.xml +++ b/functional-test/pom.xml @@ -516,6 +516,7 @@ mysql.classifier ${mysql.port} ${mysql.socket} + false diff --git a/functional-test/src/main/java/org/zanata/page/account/InactiveAccountPage.java b/functional-test/src/main/java/org/zanata/page/account/InactiveAccountPage.java index 466d25e105..765f63f251 100644 --- a/functional-test/src/main/java/org/zanata/page/account/InactiveAccountPage.java +++ b/functional-test/src/main/java/org/zanata/page/account/InactiveAccountPage.java @@ -43,12 +43,13 @@ public HomePage clickResendActivationEmail() { } public InactiveAccountPage enterNewEmail(String email) { - enterText(readyElement(By.id("inactiveAccountForm:emailField:email")), email); + enterText(readyElement(By.id("inactiveAccountForm:email:input:email")) + ,email); return new InactiveAccountPage(getDriver()); } public HomePage clickUpdateEmail() { - clickElement(By.id("inactiveAccountForm:emailField:updateEmail")); + clickElement(By.id("inactiveAccountForm:email:input:updateEmail")); return new HomePage(getDriver()); } } diff --git a/functional-test/src/main/java/org/zanata/page/account/RegisterPage.java b/functional-test/src/main/java/org/zanata/page/account/RegisterPage.java index 9e5c81b7b9..b48dbb6cf5 100644 --- a/functional-test/src/main/java/org/zanata/page/account/RegisterPage.java +++ b/functional-test/src/main/java/org/zanata/page/account/RegisterPage.java @@ -49,10 +49,10 @@ public class RegisterPage extends CorePage { public static final String USERNAME_LENGTH_ERROR = "size must be between 3 and 20"; - private By nameField = By.id("loginForm:name"); - private By emailField = By.id("loginForm:emailField:email"); - public By usernameField = By.id("loginForm:usernameField:username"); - private By passwordField = By.id("loginForm:passwordField:password"); + private By nameField = By.id("loginForm:nameField:input:name"); + private By emailField = By.id("loginForm:email:input:email"); + public By usernameField = By.id("loginForm:username:input:username"); + private By passwordField = By.id("loginForm:passwordField:input:password"); private By signUpButton = By.xpath("//input[@value='Sign Up']"); private By showHideToggleButton = By.className("js-form-password-toggle"); private By loginLink = By.linkText("Log In"); diff --git a/functional-test/src/main/java/org/zanata/page/account/ResetPasswordPage.java b/functional-test/src/main/java/org/zanata/page/account/ResetPasswordPage.java index 8f861dd874..5bd6984814 100644 --- a/functional-test/src/main/java/org/zanata/page/account/ResetPasswordPage.java +++ b/functional-test/src/main/java/org/zanata/page/account/ResetPasswordPage.java @@ -33,7 +33,7 @@ @Slf4j public class ResetPasswordPage extends BasePage { - private By usernameEmailField = By.id("passwordResetRequestForm:usernameEmailField:usernameEmail"); + private By usernameEmailField = By.id("passwordResetRequestForm:usernameEmail:input:usernameEmail"); private By submitButton = By.id("passwordResetRequestForm:submitRequest"); public ResetPasswordPage(WebDriver driver) { diff --git a/functional-test/src/main/java/org/zanata/page/administration/EditRoleAssignmentPage.java b/functional-test/src/main/java/org/zanata/page/administration/EditRoleAssignmentPage.java index d2c6f9ce3b..f3f59caa3e 100644 --- a/functional-test/src/main/java/org/zanata/page/administration/EditRoleAssignmentPage.java +++ b/functional-test/src/main/java/org/zanata/page/administration/EditRoleAssignmentPage.java @@ -32,9 +32,9 @@ @Slf4j public class EditRoleAssignmentPage extends BasePage { - private By policySelect = By.id("role-rule-form:policyNameField:policyName"); - private By patternField = By.id("role-rule-form:identityPatternField:identityPattern"); - private By roleSelect = By.id("role-rule-form:roleField:roles"); + private By policySelect = By.id("role-rule-form:policyName:input:policyName"); + private By patternField = By.id("role-rule-form:identityPattern:input:identityPattern"); + private By roleSelect = By.id("role-rule-form:role:input:roles"); private By saveButton = By.id("role-rule-form:save"); private By cancelButton = By.id("role-rule-form:cancel"); diff --git a/functional-test/src/main/java/org/zanata/page/administration/ManageUserAccountPage.java b/functional-test/src/main/java/org/zanata/page/administration/ManageUserAccountPage.java index c70630cb6e..50f7ce1187 100644 --- a/functional-test/src/main/java/org/zanata/page/administration/ManageUserAccountPage.java +++ b/functional-test/src/main/java/org/zanata/page/administration/ManageUserAccountPage.java @@ -38,8 +38,8 @@ public class ManageUserAccountPage extends BasePage { public static String PASSWORD_ERROR = "Passwords do not match"; - private By passwordField = By.id("userdetailForm:passwordField:password"); - private By passwordConfirmField = By.id("userdetailForm:passwordConfirmField:confirm"); + private By passwordField = By.id("userdetailForm:password:input:password"); + private By passwordConfirmField = By.id("userdetailForm:passwordConfirm:input:confirm"); private By enabledField = By.id("userdetailForm:enabled"); private By saveButton = By.id("userdetailForm:userdetailSave"); private By cancelButton = By.id("userdetailForm:userdetailCancel"); @@ -76,7 +76,7 @@ public ManageUserAccountPage clickEnabled() { public ManageUserAccountPage clickRole(String role) { log.info("Click role {}", role); - clickElement(readyElement(By.id("userdetailForm:rolesField:roles:" + clickElement(readyElement(By.id("userdetailForm:roles:input:roles:" .concat(roleMap.get(role))))); return new ManageUserAccountPage(getDriver()); } diff --git a/functional-test/src/main/java/org/zanata/page/administration/ServerConfigurationPage.java b/functional-test/src/main/java/org/zanata/page/administration/ServerConfigurationPage.java index 56803e0dbc..93520b9377 100644 --- a/functional-test/src/main/java/org/zanata/page/administration/ServerConfigurationPage.java +++ b/functional-test/src/main/java/org/zanata/page/administration/ServerConfigurationPage.java @@ -34,20 +34,20 @@ @Slf4j public class ServerConfigurationPage extends BasePage { - private By urlField = By.id("serverConfigForm:urlField:url"); - public static By registerUrlField = By.id("serverConfigForm:registerField:registerUrl"); + private By urlField = By.id("serverConfigForm:url:input:url"); + public static By registerUrlField = By.id("serverConfigForm:register:input:registerUrl"); private By emailDomainField = By.id("serverConfigForm:emailDomainField:emailDomain"); - private By adminEmailField = By.id("serverConfigForm:adminEmailField:adminEml"); - public static By fromEmailField = By.id("serverConfigForm:fromEmailField:fromEml"); + private By adminEmailField = By.id("serverConfigForm:adminEmail:input:adminEml"); + public static By fromEmailField = By.id("serverConfigForm:fromEmail:input:fromEml"); private By enableLogCheck = By.id("serverConfigForm:enableLogCheck"); private By logLevelSelect = By.id("serverConfigForm:logEmailLvl"); - private By emailDestinationField = By.id("serverConfigForm:logDestEmailField:logDestEml"); - private By helpUrlField = By.id("serverConfigForm:helpUrlField:helpInput"); - private By termsUrlField = By.id("serverConfigForm:termsOfUseUrlField:termsOfUseUrlEml"); - private By piwikUrl = By.id("serverConfigForm:piwikUrlField:piwikUrlEml"); + private By emailDestinationField = By.id("serverConfigForm:logDestEmail:input:logDestEml"); + private By helpUrlField = By.id("serverConfigForm:helpUrl:input:helpInput"); + private By termsUrlField = By.id("serverConfigForm:termsOfUseUrl:input:termsOfUseUrlEml"); + private By piwikUrl = By.id("serverConfigForm:piwikUrl:input:piwikUrlEml"); private By piwikId = By.id("serverConfigForm:piwikIdSiteEml"); - private By maxConcurrentField = By.id("serverConfigForm:maxConcurrentPerApiKeyField:maxConcurrentPerApiKeyEml"); - private By maxActiveField = By.id("serverConfigForm:maxActiveRequestsPerApiKeyField:maxActiveRequestsPerApiKeyEml"); + private By maxConcurrentField = By.id("serverConfigForm:maxConcurrentPerApiKey:input:maxConcurrentPerApiKeyEml"); + private By maxActiveField = By.id("serverConfigForm:maxActiveRequestsPerApiKey:input:maxActiveRequestsPerApiKeyEml"); private By saveButton = By.id("serverConfigForm:save"); public ServerConfigurationPage(WebDriver driver) { diff --git a/functional-test/src/main/java/org/zanata/page/administration/TranslationMemoryEditPage.java b/functional-test/src/main/java/org/zanata/page/administration/TranslationMemoryEditPage.java index 87d9609820..49ecde3217 100644 --- a/functional-test/src/main/java/org/zanata/page/administration/TranslationMemoryEditPage.java +++ b/functional-test/src/main/java/org/zanata/page/administration/TranslationMemoryEditPage.java @@ -32,8 +32,8 @@ @Slf4j public class TranslationMemoryEditPage extends BasePage { - private By idField = By.id("tmForm:slugField:slug"); - private By descriptionField = By.id("tmForm:descriptionField:description"); + private By idField = By.id("tmForm:slug:input:slug"); + private By descriptionField = By.id("tmForm:description:input:description"); private By saveButton = By.id("tmForm:save"); private By cancelButton = By.id("tmForm:cancel"); diff --git a/functional-test/src/main/java/org/zanata/page/dashboard/dashboardsettings/DashboardAccountTab.java b/functional-test/src/main/java/org/zanata/page/dashboard/dashboardsettings/DashboardAccountTab.java index 719d328d37..63f0fc313e 100644 --- a/functional-test/src/main/java/org/zanata/page/dashboard/dashboardsettings/DashboardAccountTab.java +++ b/functional-test/src/main/java/org/zanata/page/dashboard/dashboardsettings/DashboardAccountTab.java @@ -42,10 +42,10 @@ public class DashboardAccountTab extends DashboardBasePage { public static final String EMAIL_TAKEN_ERROR = "This email address is already taken"; - private By emailField = By.id("email-update-form:emailField:email"); + private By emailField = By.id("email-update-form:emailField:input:email"); private By updateEmailButton = By.id("email-update-form:updateEmailButton"); - private By oldPasswordField = By.id("passwordChangeForm:oldPasswordField:oldPassword"); - private By newPasswordField = By.id("passwordChangeForm:newPasswordField:newPassword"); + private By oldPasswordField = By.id("passwordChangeForm:oldPasswordField:input:oldPassword"); + private By newPasswordField = By.id("passwordChangeForm:newPasswordField:input:newPassword"); private By changePasswordButton = By.id("passwordChangeForm:changePasswordButton"); public DashboardAccountTab(WebDriver driver) { diff --git a/functional-test/src/main/java/org/zanata/page/dashboard/dashboardsettings/DashboardProfileTab.java b/functional-test/src/main/java/org/zanata/page/dashboard/dashboardsettings/DashboardProfileTab.java index 0f26c84361..8db908a478 100644 --- a/functional-test/src/main/java/org/zanata/page/dashboard/dashboardsettings/DashboardProfileTab.java +++ b/functional-test/src/main/java/org/zanata/page/dashboard/dashboardsettings/DashboardProfileTab.java @@ -32,7 +32,7 @@ @Slf4j public class DashboardProfileTab extends DashboardBasePage { - private By accountNameField = By.id("profileForm:nameField:accountName"); + private By accountNameField = By.id("profileForm:nameField:input:accountName"); private By updateProfileButton = By.id("updateProfileButton"); public DashboardProfileTab(WebDriver driver) { diff --git a/functional-test/src/main/java/org/zanata/page/groups/CreateVersionGroupPage.java b/functional-test/src/main/java/org/zanata/page/groups/CreateVersionGroupPage.java index d55ea3d673..30dc79bc1e 100644 --- a/functional-test/src/main/java/org/zanata/page/groups/CreateVersionGroupPage.java +++ b/functional-test/src/main/java/org/zanata/page/groups/CreateVersionGroupPage.java @@ -41,9 +41,9 @@ public class CreateVersionGroupPage extends BasePage { "must start and end with letter or number, and contain only " + "letters, numbers, periods, underscores and hyphens."; - private By groupIdField = By.id("group-form:slugField:slug"); - public By groupNameField = By.id("group-form:nameField:name"); - private By groupDescriptionField = By.id("group-form:descriptionField:description"); + private By groupIdField = By.id("group-form:slug:input:slug"); + public By groupNameField = By.id("group-form:name:input:name"); + private By groupDescriptionField = By.id("group-form:description:input:description"); private By saveButton = By.id("group-form:group-create-new"); private By createNewButton = By.id("group-form:group-create-new"); diff --git a/functional-test/src/main/java/org/zanata/page/languages/ContactTeamPage.java b/functional-test/src/main/java/org/zanata/page/languages/ContactTeamPage.java index d405b5cb29..f9c5459b70 100644 --- a/functional-test/src/main/java/org/zanata/page/languages/ContactTeamPage.java +++ b/functional-test/src/main/java/org/zanata/page/languages/ContactTeamPage.java @@ -31,7 +31,7 @@ @Slf4j public class ContactTeamPage extends BasePage { - private By messageField = By.id("contactCoordinatorForm:messageField:contact-coordinator-message"); + private By messageField = By.id("contactCoordinatorForm:messageField:input:contact-coordinator-message"); private By sendButton = By.id("contact-coordinator-send-button"); public ContactTeamPage(WebDriver driver) { diff --git a/functional-test/src/main/java/org/zanata/page/projects/CreateProjectPage.java b/functional-test/src/main/java/org/zanata/page/projects/CreateProjectPage.java index af2c150e9d..75483b5485 100644 --- a/functional-test/src/main/java/org/zanata/page/projects/CreateProjectPage.java +++ b/functional-test/src/main/java/org/zanata/page/projects/CreateProjectPage.java @@ -31,9 +31,9 @@ @Slf4j public class CreateProjectPage extends BasePage { - private By idField = By.id("project-form:slugField:slug"); - private By nameField = By.id("project-form:nameField:name"); - private By descriptionField = By.id("project-form:descriptionField:description"); + private By idField = By.id("project-form:slug:input:slug"); + private By nameField = By.id("project-form:name:input:name"); + private By descriptionField = By.id("project-form:description:input:description"); private By projectTypeList = By.id("project-types"); private By createButton = By.id("project-form:create-new"); diff --git a/functional-test/src/main/java/org/zanata/page/projects/ProjectsPage.java b/functional-test/src/main/java/org/zanata/page/projects/ProjectsPage.java index 576f67bc04..7792994435 100644 --- a/functional-test/src/main/java/org/zanata/page/projects/ProjectsPage.java +++ b/functional-test/src/main/java/org/zanata/page/projects/ProjectsPage.java @@ -41,7 +41,7 @@ public class ProjectsPage extends BasePage { private By createProjectButton = By.id("createProjectLink"); private By mainContentDiv = By.id("main_body_content"); - private By projectTable = By.id("main_content:form:projectList"); + private By projectTable = By.id("form:projectList"); private By activeCheckBox = By.xpath("//*[@data-original-title='Filter active projects']"); private By readOnlyCheckBox = By.xpath("//*[@data-original-title='Filter read-only projects']"); diff --git a/functional-test/src/main/java/org/zanata/page/projects/projectsettings/ProjectGeneralTab.java b/functional-test/src/main/java/org/zanata/page/projects/projectsettings/ProjectGeneralTab.java index 53ed50ed36..0298636e1e 100644 --- a/functional-test/src/main/java/org/zanata/page/projects/projectsettings/ProjectGeneralTab.java +++ b/functional-test/src/main/java/org/zanata/page/projects/projectsettings/ProjectGeneralTab.java @@ -38,12 +38,12 @@ @Slf4j public class ProjectGeneralTab extends ProjectBasePage { - private By projectIdField = By.id("settings-general-form:slugField:slug"); - private By projectNameField = By.id("settings-general-form:nameField:name"); - private By descriptionField = By.id("settings-general-form:descriptionField:description"); + private By projectIdField = By.id("settings-general-form:slug:input:slug"); + private By projectNameField = By.id("settings-general-form:name:input:name"); + private By descriptionField = By.id("settings-general-form:description:input:description"); private By projectTypeList = By.id("project-types"); - private By homepageField = By.id("settings-general-form:homePageField:homePage"); - private By repoField = By.id("settings-general-form:repoField:repo"); + private By homepageField = By.id("settings-general-form:homePage:input:homePage"); + private By repoField = By.id("settings-general-form:repo:input:repo"); private By deleteButton = By.id("button-archive-project"); private By confirmDeleteButton = By.id("deleteButton"); private By confirmDeleteInput = By.id("confirmDeleteInput"); diff --git a/functional-test/src/main/java/org/zanata/page/projectversion/CreateVersionPage.java b/functional-test/src/main/java/org/zanata/page/projectversion/CreateVersionPage.java index 992638ef47..f873f35a59 100644 --- a/functional-test/src/main/java/org/zanata/page/projectversion/CreateVersionPage.java +++ b/functional-test/src/main/java/org/zanata/page/projectversion/CreateVersionPage.java @@ -37,7 +37,7 @@ public class CreateVersionPage extends BasePage { "must start and end with letter or number, and contain only " + "letters, numbers, periods, underscores and hyphens."; - public By projectVersionID = By.id("create-version-form:slugField:slug"); + public By projectVersionID = By.id("create-version-form:slug:input:slug"); private By projectTypeSelection = By.id("create-version-form:project-type"); private By saveButton = By.id("create-version-form:button-create"); private By copyFromPreviousVersionChk = By.id("create-version-form:copy-from-version"); diff --git a/functional-test/src/main/java/org/zanata/page/utility/ContactAdminFormPage.java b/functional-test/src/main/java/org/zanata/page/utility/ContactAdminFormPage.java index 72fc963246..a8a80f9a6c 100644 --- a/functional-test/src/main/java/org/zanata/page/utility/ContactAdminFormPage.java +++ b/functional-test/src/main/java/org/zanata/page/utility/ContactAdminFormPage.java @@ -34,7 +34,7 @@ public class ContactAdminFormPage extends BasePage { private By subjectField = By.id("contactAdminForm:subjectField:subject"); - private By messageField = By.id("contactAdminForm:messageField:contact-admin-message"); + private By messageField = By.id("contactAdminForm:messageField:input:contact-admin-message"); private By sendButton = By.id("contact-admin-send-button"); public ContactAdminFormPage(WebDriver driver) { diff --git a/functional-test/src/test/java/org/zanata/feature/versionGroup/VersionGroupTest.java b/functional-test/src/test/java/org/zanata/feature/versionGroup/VersionGroupTest.java index b7e7569084..6d2ac4ceda 100644 --- a/functional-test/src/test/java/org/zanata/feature/versionGroup/VersionGroupTest.java +++ b/functional-test/src/test/java/org/zanata/feature/versionGroup/VersionGroupTest.java @@ -46,9 +46,6 @@ @Category(DetailedTest.class) public class VersionGroupTest extends ZanataTestCase { -// @ClassRule -// public static AddUsersRule addUsersRule = new AddUsersRule(); - @ClassRule public static SampleProjectRule sampleProjectRule = new SampleProjectRule(); diff --git a/zanata-war/pom.xml b/zanata-war/pom.xml index 229bfd102b..193283b929 100644 --- a/zanata-war/pom.xml +++ b/zanata-war/pom.xml @@ -160,7 +160,6 @@ com.google.guava:guava-gwt - org.jboss.spec.javax.el:jboss-el-api_2.2_spec javax.servlet.jsp:jsp-api org.apache.solr:solr-core diff --git a/zanata-war/src/main/java/org/zanata/action/UserAction.java b/zanata-war/src/main/java/org/zanata/action/UserAction.java index 44547890a3..cefecd934d 100644 --- a/zanata-war/src/main/java/org/zanata/action/UserAction.java +++ b/zanata-war/src/main/java/org/zanata/action/UserAction.java @@ -188,7 +188,7 @@ public String save() { private String saveNewUser() { if (password == null || !password.equals(confirm)) { - StatusMessages.instance().addToControl("password", "Passwords do not match"); + facesMessages.addToControl("password", "Passwords do not match"); return "failure"; } @@ -211,7 +211,7 @@ private String saveExistingUser() { // Check if a new password has been entered if (password != null && !"".equals(password)) { if (!password.equals(confirm)) { - StatusMessages.instance().addToControl("password", "Passwords do not match"); + facesMessages.addToControl("password", "Passwords do not match"); return "failure"; } else { identityManager.changePassword(username, password); diff --git a/zanata-war/src/main/java/org/zanata/ui/UIInputContainer.java b/zanata-war/src/main/java/org/zanata/ui/UIInputContainer.java new file mode 100644 index 0000000000..fb535bd934 --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/ui/UIInputContainer.java @@ -0,0 +1,469 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011, Red Hat, Inc., and individual contributors + * by the @authors tag. See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zanata.ui; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.faces.FacesException; +import javax.faces.application.FacesMessage; +import javax.faces.component.EditableValueHolder; +import javax.faces.component.FacesComponent; +import javax.faces.component.NamingContainer; +import javax.faces.component.UIComponent; +import javax.faces.component.UIComponentBase; +import javax.faces.component.UIMessage; +import javax.faces.component.UINamingContainer; +import javax.faces.component.UIViewRoot; +import javax.faces.component.html.HtmlOutputLabel; +import javax.faces.context.FacesContext; +import javax.faces.validator.BeanValidator; +import javax.validation.Validation; +import javax.validation.ValidationException; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; + +/** + * N.B. This class is copied from seam's migration demo project at https://github.com/seam/migration. Modified to suit us. + * + * UIInputContainer is a supplemental component for a JSF 2.0 composite component encapsulating one or more + * input components (EditableValueHolder), their corresponding message components (UIMessage) + * and a label (HtmlOutputLabel). This component takes care of wiring the label to the first input and the + * messages to each input in sequence. It also assigns two implicit attribute values, "required" and "invalid" to indicate that + * a required input field is present and whether there are any validation errors, respectively. To determine if a input field is + * required, both the required attribute is consulted. Finally, if the + * "label" attribute is not provided on the composite component, the label value will be derived from the id of the composite + * component, for convenience. + *

+ *

+ * Composite component definition example (minus layout): + *

+ *

+ *

+ *

+ * <cc:interface componentType="org.jboss.seam.faces.InputContainer"/>
+ * <cc:implementation>
+ *   <h:outputLabel id="label" value="#{cc.attrs.label}:" styleClass="#{cc.attrs.invalid ? 'invalid' : ''}">
+ *     <h:ouputText styleClass="required" rendered="#{cc.attrs.required}" value="*"/>
+ *   </h:outputLabel>
+ *   <cc:insertChildren/>
+ *   <h:message id="message" errorClass="invalid message" rendered="#{cc.attrs.invalid}"/>
+ * </cc:implementation>
+ * 
+ *

+ *

+ * Composite component usage example: + *

+ *

+ *

+ *

+ * <example:inputContainer id="name">
+ *   <h:inputText id="input" value="#{person.name}"/>
+ * </example:inputContainer>
+ * 
+ *

+ *

+ * Possible enhancements: + *

+ * + *

+ *

+ * NOTE: Firefox does not properly associate a label with the target input if the input id contains a colon (:), the default + * separator character in JSF. JSF 2 allows developers to set the value via an initialization parameter (context-param in + * web.xml) keyed to javax.faces.SEPARATOR_CHAR. We recommend that you override this setting to make the separator an underscore + * (_). + *

+ * + * @author Dan Allen + * @author Jose Rodolfo freitas + */ +@FacesComponent(UIInputContainer.COMPONENT_TYPE) +public class UIInputContainer extends UIComponentBase implements NamingContainer { + /** + * The standard component type for this component. + */ + public static final String COMPONENT_TYPE = "org.zanata.faces.InputContainer"; + + protected static final String HTML_ID_ATTR_NAME = "id"; + protected static final String HTML_CLASS_ATTR_NAME = "class"; + protected static final String HTML_STYLE_ATTR_NAME = "style"; + + private boolean beanValidationPresent = false; + + public UIInputContainer() { + beanValidationPresent = isClassPresent("javax.validation.Validator"); + } + + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + public Validator getValidator() { + return validator; + } + + public void setValidator(Validator validator) { + this.validator = validator; + } + + @Override + public String getFamily() { + return UINamingContainer.COMPONENT_FAMILY; + } + + /** + * The name of the auto-generated composite component attribute that holds a boolean indicating whether the the template + * contains an invalid input. + */ + public String getInvalidAttributeName() { + return "invalid"; + } + + /** + * The name of the auto-generated composite component attribute that holds a boolean indicating whether the template + * contains a required input. + */ + public String getRequiredAttributeName() { + return "required"; + } + + /** + * The name of the composite component attribute that holds the string label for this set of inputs. If the label attribute + * is not provided, one will be generated from the id of the composite component or, if the id is defaulted, the name of the + * property bound to the first input. + */ + public String getLabelAttributeName() { + return "label"; + } + + /** + * The name of the auto-generated composite component attribute that holds the elements in this input container. The + * elements include the label, a list of inputs and a cooresponding list of messages. + */ + public String getElementsAttributeName() { + return "elements"; + } + + /** + * The name of the composite component attribute that holds a boolean indicating whether the component template should be + * enclosed in an HTML element, so that it be referenced from JavaScript. + */ + public String getEncloseAttributeName() { + return "enclose"; + } + + public String getContainerElementName() { + return "div"; + } + + public String getDefaultLabelId() { + return "label"; + } + + public String getDefaultInputId() { + return "input"; + } + + public String getDefaultMessageId() { + return "message"; + } + + @Override + public void encodeBegin(final FacesContext context) throws IOException { + if (!isRendered()) { + return; + } + + getAttributes().put(getInvalidAttributeName(), false); + super.encodeBegin(context); + + InputContainerElements elements = scan(getFacet(UIComponent.COMPOSITE_FACET_NAME), null, context); + // assignIds(elements, context); + wire(elements, context); + + getAttributes().put(getElementsAttributeName(), elements); + + if (elements.hasValidationError()) { + getAttributes().put(getInvalidAttributeName(), true); + } + for (EditableValueHolder input : elements.getInputs()) { + // if we use FacesMessages.addToControl(id, message) directly in + // code, e.g. testing whether some value is unique in database, we + // need to set invalid to true in order to display the message. We + // also need to add the message to input client id because that's + // what's being set as for for h:message (see + // org.zanata.ui.UIInputContainer.InputContainerElements.wire). + Iterator messagesForInput = context.getMessages( + ((UIComponent) input).getClientId(context)); + if (messagesForInput.hasNext()) { + getAttributes().put(getInvalidAttributeName(), true); + } + } + + getAttributes().put(getRequiredAttributeName(), elements.hasRequiredInput()); + + /* + * for some reason, Mojarra is not filling Attribute Map with "label" key if label attr has an EL value, so I added a + * labelHasEmptyValue to guarantee that there was no label setted. + */ + if (getValueExpression(getLabelAttributeName()) == null + && (!getAttributes().containsKey(getLabelAttributeName()) || labelHasEmptyValue(elements))) { + getAttributes().put(getLabelAttributeName(), generateLabel(elements, context)); + } + + if (Boolean.TRUE.equals(getAttributes().get(getEncloseAttributeName()))) { + startContainerElement(context); + } + } + + @Override + public void encodeEnd(final FacesContext context) throws IOException { + if (!isRendered()) { + return; + } + + super.encodeEnd(context); + + if (Boolean.TRUE.equals(getAttributes().get(getEncloseAttributeName()))) { + endContainerElement(context); + } + } + + protected void startContainerElement(final FacesContext context) throws IOException { + context.getResponseWriter().startElement(getContainerElementName(), this); + String style = (getAttributes().get("style") != null ? getAttributes().get("style").toString().trim() : null); + if (style.length() > 0) { + context.getResponseWriter().writeAttribute(HTML_STYLE_ATTR_NAME, style, HTML_STYLE_ATTR_NAME); + } + String styleClass = (getAttributes().get("styleClass") != null ? getAttributes().get("styleClass").toString().trim() + : null); + if (styleClass.length() > 0) { + context.getResponseWriter().writeAttribute(HTML_CLASS_ATTR_NAME, styleClass, HTML_CLASS_ATTR_NAME); + } + context.getResponseWriter().writeAttribute(HTML_ID_ATTR_NAME, getClientId(context), HTML_ID_ATTR_NAME); + } + + protected void endContainerElement(final FacesContext context) throws IOException { + context.getResponseWriter().endElement(getContainerElementName()); + } + + protected String generateLabel(final InputContainerElements elements, final FacesContext context) { + String name = getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX) ? elements.getPropertyName(context) : getId(); + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + + /** + * Walk the component tree branch built by the composite component and locate the input container elements. + * + * @return a composite object of the input container elements + */ + protected InputContainerElements scan(final UIComponent component, InputContainerElements elements, + final FacesContext context) { + if (elements == null) { + elements = new InputContainerElements(); + } + + // NOTE we need to walk the tree ignoring rendered attribute because it's condition + // could be based on what we discover + if ((elements.getLabel() == null) && (component instanceof HtmlOutputLabel)) { + elements.setLabel((HtmlOutputLabel) component); + } else if (component instanceof EditableValueHolder) { + elements.registerInput((EditableValueHolder) component, getDefaultValidator(context), context); + } else if (component instanceof UIMessage) { + elements.registerMessage((UIMessage) component); + } + // may need to walk smarter to ensure "element of least suprise" + for (UIComponent child : component.getChildren()) { + scan(child, elements, context); + } + + return elements; + } + + // assigning ids seems to break form submissions, but I don't know why + public void assignIds(final InputContainerElements elements, final FacesContext context) { + boolean refreshIds = false; + if (getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) { + setId(elements.getPropertyName(context)); + refreshIds = true; + } + UIComponent label = elements.getLabel(); + if (label != null) { + if (label.getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) { + label.setId(getDefaultLabelId()); + } else if (refreshIds) { + label.setId(label.getId()); + } + } + for (int i = 0, len = elements.getInputs().size(); i < len; i++) { + UIComponent input = (UIComponent) elements.getInputs().get(i); + if (input.getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) { + input.setId(getDefaultInputId() + (i == 0 ? "" : (i + 1))); + } else if (refreshIds) { + input.setId(input.getId()); + } + } + for (int i = 0, len = elements.getMessages().size(); i < len; i++) { + UIComponent msg = elements.getMessages().get(i); + if (msg.getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) { + msg.setId(getDefaultMessageId() + (i == 0 ? "" : (i + 1))); + } else if (refreshIds) { + msg.setId(msg.getId()); + } + } + } + + /** + * Wire the label and messages to the input(s) + */ + protected void wire(final InputContainerElements elements, final FacesContext context) { + elements.wire(context); + } + + /** + * Get the default Bean Validation Validator to read the contraints for a property. + */ + private Validator getDefaultValidator(final FacesContext context) throws FacesException { + if (!beanValidationPresent) { + return null; + } + + ValidatorFactory validatorFactory; + Object cachedObject = context.getExternalContext().getApplicationMap().get(BeanValidator.VALIDATOR_FACTORY_KEY); + if (cachedObject instanceof ValidatorFactory) { + validatorFactory = (ValidatorFactory) cachedObject; + } else { + try { + validatorFactory = Validation.buildDefaultValidatorFactory(); + } catch (ValidationException e) { + throw new FacesException("Could not build a default Bean Validator factory", e); + } + context.getExternalContext().getApplicationMap().put(BeanValidator.VALIDATOR_FACTORY_KEY, validatorFactory); + } + return validatorFactory.getValidator(); + } + + private boolean isClassPresent(final String fqcn) { + try { + if (Thread.currentThread().getContextClassLoader() != null) { + return Thread.currentThread().getContextClassLoader().loadClass(fqcn) != null; + } else { + return Class.forName(fqcn) != null; + } + } catch (ClassNotFoundException e) { + return false; + } catch (NoClassDefFoundError e) { + return false; + } + } + + private boolean labelHasEmptyValue(InputContainerElements elements) { + if (elements.getLabel() == null || elements.getLabel().getValue() == null) + return false; + return (elements.getLabel().getValue().toString().trim().equals(":") || elements.getLabel().getValue().toString() + .trim().equals("")); + } + + public static class InputContainerElements { + private String propertyName; + private HtmlOutputLabel label; + private final List inputs = new ArrayList(); + private final List messages = new ArrayList(); + private boolean validationError = false; + + public HtmlOutputLabel getLabel() { + return label; + } + + public void setLabel(final HtmlOutputLabel label) { + this.label = label; + } + + public List getInputs() { + return inputs; + } + + public void registerInput(final EditableValueHolder input, final Validator validator, final FacesContext context) { + inputs.add(input); + + if (!input.isValid()) { + validationError = true; + } else if (!validationError) { + // optimization to avoid loop if already flagged + Iterator it = context.getMessages(((UIComponent) input).getClientId(context)); + while (it.hasNext()) { + if (it.next().getSeverity().compareTo(FacesMessage.SEVERITY_WARN) >= 0) { + validationError = true; + break; + } + } + } + } + + public List getMessages() { + return messages; + } + + public void registerMessage(final UIMessage message) { + messages.add(message); + } + + public boolean hasValidationError() { + return validationError; + } + + public boolean hasRequiredInput() { + //We have to scan these each time as the value could change in an AJAX request + for (EditableValueHolder holder : inputs) { + if (holder.isRequired()) { + return true; + } + } + return false; + } + + public String getPropertyName(final FacesContext context) { + if (propertyName != null) { + return propertyName; + } + + if (inputs.size() == 0) { + return null; + } + + propertyName = (String) new ValueExpressionAnalyzer(((UIComponent) inputs.get(0)).getValueExpression("value")) + .getValueReference(context.getELContext()).getProperty(); + return propertyName; + } + + public void wire(final FacesContext context) { + int numInputs = inputs.size(); + if (numInputs > 0) { + if (label != null) { + label.setFor(((UIComponent) inputs.get(0)).getClientId(context)); + } + for (int i = 0, len = messages.size(); i < len; i++) { + if (i < numInputs) { + messages.get(i).setFor(((UIComponent) inputs.get(i)).getClientId(context)); + } + } + } + } + } +} diff --git a/zanata-war/src/main/java/org/zanata/ui/ValueExpressionAnalyzer.java b/zanata-war/src/main/java/org/zanata/ui/ValueExpressionAnalyzer.java new file mode 100644 index 0000000000..cc7c96012e --- /dev/null +++ b/zanata-war/src/main/java/org/zanata/ui/ValueExpressionAnalyzer.java @@ -0,0 +1,177 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2011, Red Hat, Inc., and individual contributors + * by the @authors tag. See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zanata.ui; + +import java.beans.FeatureDescriptor; +import java.util.Iterator; +import java.util.Locale; + +import javax.el.ELContext; +import javax.el.ELException; +import javax.el.ELResolver; +import javax.el.FunctionMapper; +import javax.el.ValueExpression; +import javax.el.ValueReference; +import javax.el.VariableMapper; +import javax.faces.el.CompositeComponentExpressionHolder; + +/** + * N.B. This class is copied from seam's migration demo project at https://github.com/seam/migration. + *

+ * Analyzes a {@link ValueExpression} and provides access to the base object and property name referenced by the expression. + *

+ *

+ *

+ * The getValueReference(ELContext) method returns a {@link ValueReference} object, which encapsulates the base object and + * property name to which the expression maps. This process works by resolving the expression up until the last segment. + *

+ *

+ *

+ * Although access to the ValueReference was added in EL 2.2, the feature does not work correctly, which is why this custom + * class is required. + *

+ * + * @author Dan Allen + */ +class ValueExpressionAnalyzer { + private ValueExpression expression; + + public ValueExpressionAnalyzer(ValueExpression expression) { + this.expression = expression; + } + + public ValueReference getValueReference(ELContext elContext) { + InterceptingResolver resolver = new InterceptingResolver(elContext.getELResolver()); + try { + expression.setValue(decorateELContext(elContext, resolver), null); + } catch (ELException ele) { + return null; + } + ValueReference reference = resolver.getValueReference(); + if (reference != null) { + Object base = reference.getBase(); + if (base instanceof CompositeComponentExpressionHolder) { + ValueExpression ve = ((CompositeComponentExpressionHolder) base) + .getExpression((String) reference.getProperty()); + if (ve != null) { + this.expression = ve; + reference = getValueReference(elContext); + } + } + } + return reference; + } + + private ELContext decorateELContext(final ELContext context, final ELResolver resolver) { + return new ELContext() { + // punch in our new ELResolver + @Override + public ELResolver getELResolver() { + return resolver; + } + + // The rest of the methods simply delegate to the existing context + @Override + public Object getContext(Class key) { + return context.getContext(key); + } + + @Override + public Locale getLocale() { + return context.getLocale(); + } + + @Override + public boolean isPropertyResolved() { + return context.isPropertyResolved(); + } + + @Override + public void putContext(Class key, Object contextObject) { + context.putContext(key, contextObject); + } + + @Override + public void setLocale(Locale locale) { + context.setLocale(locale); + } + + @Override + public void setPropertyResolved(boolean resolved) { + context.setPropertyResolved(resolved); + } + + @Override + public FunctionMapper getFunctionMapper() { + return context.getFunctionMapper(); + } + + @Override + public VariableMapper getVariableMapper() { + return context.getVariableMapper(); + } + }; + } + + private static class InterceptingResolver extends ELResolver { + private ELResolver delegate; + private ValueReference valueReference; + + public InterceptingResolver(ELResolver delegate) { + this.delegate = delegate; + } + + public ValueReference getValueReference() { + return valueReference; + } + + // Capture the base and property rather than write the value + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + if (base != null && property != null) { + context.setPropertyResolved(true); + valueReference = new ValueReference(base, property.toString()); + } + } + + // The rest of the methods simply delegate to the existing context + @Override + public Object getValue(ELContext context, Object base, Object property) { + return delegate.getValue(context, base, property); + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + return delegate.getType(context, base, property); + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + return delegate.isReadOnly(context, base, property); + } + + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + return delegate.getFeatureDescriptors(context, base); + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + return delegate.getCommonPropertyType(context, base); + } + } +} diff --git a/zanata-war/src/main/java/org/zanata/ui/faces/FacesMessages.java b/zanata-war/src/main/java/org/zanata/ui/faces/FacesMessages.java index faae0c26c7..3a6c8e1283 100644 --- a/zanata-war/src/main/java/org/zanata/ui/faces/FacesMessages.java +++ b/zanata-war/src/main/java/org/zanata/ui/faces/FacesMessages.java @@ -27,11 +27,15 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; +import java.util.Stack; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; +import javax.faces.component.UIViewRoot; import javax.faces.context.FacesContext; import org.jboss.seam.ScopeType; @@ -43,6 +47,7 @@ import org.jboss.seam.core.Interpolator; import org.zanata.i18n.Messages; import org.zanata.util.ServiceLocator; +import com.google.common.collect.Lists; /** * Utility to allow for easy handling of JSF messages. Serves as a replacement @@ -87,26 +92,39 @@ public void beforeRenderResponse() { */ private String getClientId(String id) { FacesContext facesContext = FacesContext.getCurrentInstance(); - return getClientId(facesContext.getViewRoot(), id, facesContext); - } - private static String getClientId(UIComponent component, String id, - FacesContext facesContext) { - String componentId = component.getId(); - if (componentId != null && componentId.equals(id)) { - return component.getClientId(facesContext); - } else { - Iterator iter = component.getFacetsAndChildren(); - while (iter.hasNext()) { - UIComponent child = (UIComponent) iter.next(); - String clientId = getClientId(child, id, facesContext); - if (clientId != null) - return clientId; + // we search from backwards, so for a component tree A->B->C, we search + // id from C then B then A for a match of id. If we found + // C.getId().equals(id), we will use C.getClientId() + Stack uiComponentStack = new Stack<>(); + addComponentToStack(uiComponentStack, facesContext.getViewRoot()); + + while (!uiComponentStack.empty()) { + UIComponent pop = uiComponentStack.pop(); + if (pop.getId() != null && id.equals(pop.getId())) { + return pop.getClientId(); } - return null; } + return null; } + private void addComponentToStack(Stack uiComponentStack, + UIComponent component) { + uiComponentStack.push(component); + Iterator iter = component.getFacetsAndChildren(); + + Queue children = new LinkedList<>(); + while (iter.hasNext()) { + UIComponent next = iter.next(); + uiComponentStack.push(next); + children.add(next); + } + for (UIComponent child : children) { + addComponentToStack(uiComponentStack, child); + } + } + + /** * Add a status message, looking up the message in the resource bundle using * the provided key. If the message is found, it is used, otherwise, the diff --git a/zanata-war/src/main/webapp/WEB-INF/layout/admin/contact_admin_modal.xhtml b/zanata-war/src/main/webapp/WEB-INF/layout/admin/contact_admin_modal.xhtml index 063e1aea28..713c55e8bc 100644 --- a/zanata-war/src/main/webapp/WEB-INF/layout/admin/contact_admin_modal.xhtml +++ b/zanata-war/src/main/webapp/WEB-INF/layout/admin/contact_admin_modal.xhtml @@ -3,6 +3,7 @@ xmlns:s="http://jboss.org/schema/seam/taglib" xmlns:rich="http://richfaces.org/rich" xmlns:a4j="http://richfaces.org/a4j" + xmlns:zanata="http://java.sun.com/jsf/composite/zanata" class="modal" id="contactAdminDialog" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> @@ -57,12 +58,12 @@ oncomplete="getContactAdminForm().find('#contact-admin-cancel-button').click()"/>