/** * Copyright (c) 2000-2017 Liferay, Inc. All rights reserved. * * 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 com.liferay.faces.util.context.internal; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.faces.FactoryFinder; import javax.faces.application.FacesMessage; import javax.faces.component.EditableValueHolder; import javax.faces.component.UIComponent; import javax.faces.component.visit.VisitCallback; import javax.faces.component.visit.VisitContext; import javax.faces.component.visit.VisitContextFactory; import javax.faces.component.visit.VisitHint; import javax.faces.component.visit.VisitResult; import javax.faces.context.FacesContext; import javax.faces.context.PartialViewContext; import javax.faces.context.PartialViewContextWrapper; import javax.faces.event.PhaseId; import com.liferay.faces.util.logging.Logger; import com.liferay.faces.util.logging.LoggerFactory; /** * <p>This class is a wrapper around the {@link PartialViewContext}. Its purpose is to provide a way to provide a way * for components to handle validation for required fields with "onchange" behavior rather than "onblur" behavior. For * example, the ICEfaces <a href="http://res.icesoft.org/docs/v3_latest/ace/tld/ace/textEntry.html">ace:textEntry</a> * component only suppors the "onblur" event when used with <a * href="http://res.icesoft.org/docs/v3_latest/ace/tld/ace/ajax.html">ace:ajax</a>. In the case of required="true", the * JSF PROCESS_VALIDATIONS phase will add a {@link EditableValueHolder#REQUIRED_MESSAGE_ID} {@link FacesMessage} when * the user tabs-out of the field. By specifying the following in the WEB-INF/faces-config.xml descriptor, the * FacesMessages will be removed which approximates the behavior of the "onchange" event:</p> * * <pre><partial-view-context-factory>com.liferay.faces.util.context.PartialViewContextFactoryOnChangeImpl</partial-view-context-factory></pre> * * @author Neil Griffin */ public class PartialViewContextOnChangeImpl extends PartialViewContextWrapper { // Logger private static final Logger logger = LoggerFactory.getLogger(PartialViewContextOnChangeImpl.class); // Private Data Members private FacesContext facesContext; private PartialViewContext wrappedPartialViewContext; private Map<String, Object> valueMap; private Map<String, Boolean> validMap; public PartialViewContextOnChangeImpl(PartialViewContext partialViewContext, FacesContext facesContext) { this.wrappedPartialViewContext = partialViewContext; this.facesContext = facesContext; this.validMap = new HashMap<String, Boolean>(); this.valueMap = new HashMap<String, Object>(); } @Override public PartialViewContext getWrapped() { return wrappedPartialViewContext; } @Override public void processPartial(PhaseId phaseId) { // If processing a partial request during the "Apply Request Values" phase of the JSF lifecycle, then register // a callback that will save-off the values of the components that are visited in the tree-walk. if (phaseId == PhaseId.APPLY_REQUEST_VALUES) { VisitContextFactory visitContextFactory = (VisitContextFactory) FactoryFinder.getFactory( FactoryFinder.VISIT_CONTEXT_FACTORY); Collection<String> renderIds = wrappedPartialViewContext.getExecuteIds(); EnumSet<VisitHint> visitHints = EnumSet.of(VisitHint.EXECUTE_LIFECYCLE); VisitContext visitContext = visitContextFactory.getVisitContext(facesContext, renderIds, visitHints); VisitCallback visitCallback = new VisitCallbackApplyRequestValuesImpl(valueMap, validMap); facesContext.getViewRoot().visitTree(visitContext, visitCallback); } // Otherwise, if processing a partial request during the "Render Response" phase of the JSF lifecycle, then // register a callback that will remove extraneous FacesMessages for components that are visited in the // tree-walk. else if (phaseId == PhaseId.RENDER_RESPONSE) { VisitContextFactory visitContextFactory = (VisitContextFactory) FactoryFinder.getFactory( FactoryFinder.VISIT_CONTEXT_FACTORY); Collection<String> renderIds = wrappedPartialViewContext.getExecuteIds(); EnumSet<VisitHint> visitHints = EnumSet.of(VisitHint.EXECUTE_LIFECYCLE); VisitContext visitContext = visitContextFactory.getVisitContext(facesContext, renderIds, visitHints); VisitCallback visitCallback = new VisitCallbackRenderResponseImpl(facesContext, valueMap, validMap); facesContext.getViewRoot().visitTree(visitContext, visitCallback); } // Ask the delegation chain to continue partial request processing. super.processPartial(phaseId); } /** * This method is missing from the {@link PartialViewContextWrapper} class so it must be implemented here. */ @Override public void setPartialRequest(boolean isPartialRequest) { wrappedPartialViewContext.setPartialRequest(isPartialRequest); } private static class VisitCallbackApplyRequestValuesImpl implements VisitCallback { private Map<String, Object> valueMap; private Map<String, Boolean> validMap; public VisitCallbackApplyRequestValuesImpl(Map<String, Object> valueMap, Map<String, Boolean> validMap) { this.validMap = validMap; this.valueMap = valueMap; } public VisitResult visit(VisitContext visitContext, UIComponent uiComponent) { if (uiComponent instanceof EditableValueHolder) { EditableValueHolder editableValueHolder = (EditableValueHolder) uiComponent; String clientId = uiComponent.getClientId(); validMap.put(clientId, editableValueHolder.isValid()); valueMap.put(clientId, editableValueHolder.getValue()); } // Indicate to the caller that the tree walk should continue and that the specified component's subtree // should be processed as well. return VisitResult.ACCEPT; } } private static class VisitCallbackRenderResponseImpl implements VisitCallback { private FacesContext facesContext; private Map<String, Object> valueMap; private Map<String, Boolean> validMap; public VisitCallbackRenderResponseImpl(FacesContext facesContext, Map<String, Object> valueMap, Map<String, Boolean> validMap) { this.facesContext = facesContext; this.validMap = validMap; this.valueMap = valueMap; } public VisitResult visit(VisitContext visitContext, UIComponent uiComponent) { // If the specified component participates in input validation, then if (uiComponent instanceof EditableValueHolder) { // Temporarily push the component to the EL stack so that the following call to isRequired() will work // with EL expressions. EditableValueHolder editableValueHolder = (EditableValueHolder) uiComponent; uiComponent.pushComponentToEL(facesContext, uiComponent); boolean required = editableValueHolder.isRequired(); uiComponent.popComponentFromEL(facesContext); // If the specified component is required, then if (required) { String clientId = uiComponent.getClientId(); Object previousValue = valueMap.get(clientId); Object submittedValue = editableValueHolder.getSubmittedValue(); boolean submittedValueEmpty = ((submittedValue == null) || "".equals(submittedValue)); boolean previouslyValid = validMap.get(clientId); logger.debug( "previousValue=[{0}] submittedValue=[{1}] submittedValueEmpty=[{2}] previouslyValid=[{3}]", previousValue, submittedValue, submittedValueEmpty, previouslyValid); // If the user hasn't provided value yet, then remove any FacesMessages associated with the // component. This will prevent "Value is required" type messages from being displayed to the user // when they simply tab-out of a field that triggered an "onblur" event. if ((previousValue == null) && submittedValueEmpty && previouslyValid) { List<FacesMessage> messageList = facesContext.getMessageList(clientId); if (messageList != null) { List<FacesMessage> facesMessagesForClientId = new ArrayList<FacesMessage>(messageList); for (FacesMessage facesMessage : facesMessagesForClientId) { Iterator<FacesMessage> allFacesMessagesItr = facesContext.getMessages(); while (allFacesMessagesItr.hasNext()) { FacesMessage curFacesMessage = allFacesMessagesItr.next(); if (facesMessage.equals(curFacesMessage)) { allFacesMessagesItr.remove(); editableValueHolder.setValid(true); if (logger.isDebugEnabled()) { String summary = facesMessage.getSummary(); logger.debug("Removed facesMessage summary=[{0}] for clientId=[{1}]", summary, clientId); } } } } } } } } // Indicate to the caller that the tree walk should continue and that the specified component's subtree // should be processed as well. return VisitResult.ACCEPT; } } }