/* * Copyright 2017 OmniFaces * * 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.omnifaces.eventlistener; import static org.omnifaces.util.Events.subscribeToApplicationEvent; import static org.omnifaces.util.Utils.isEmpty; import java.util.LinkedHashSet; import java.util.Set; import javax.faces.component.UICommand; import javax.faces.component.UIComponent; import javax.faces.component.UIForm; import javax.faces.component.UIInput; import javax.faces.component.UIViewRoot; import javax.faces.context.FacesContext; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseId; import javax.faces.event.PostValidateEvent; import javax.faces.event.SystemEvent; import javax.faces.event.SystemEventListener; import org.omnifaces.event.PostInvokeActionEvent; import org.omnifaces.event.PreInvokeActionEvent; /** * <p> * The {@link InvokeActionEventListener} will add support for new <code><f:event></code> types * <code>preInvokeAction</code> and <code>postInvokeAction</code>. Those events are published during the beforephase and * afterphase of <code>INVOKE_APPLICATION</code> respectively. This actually offers a better hook on invoking actions * after the <code><f:viewParam></code> values been set than the <code>preRenderView</code> event. In some * circumstances the <code>preRenderView</code> event might be too late. For example, when you need to set a faces * message in the flash scope and send a redirect. Also, it won't be invoked when the validations phase has failed for * one of the <code><f:viewParam></code> values, in contrary to the <code>preRenderView</code> event. * <p> * Note that the upcoming JSF 2.2 will come with a <code><f:viewAction></code> tag which should actually solve * the concrete functional requirement for which a <code><f:event type="preRenderView"></code> workaround is often * been used in JSF 2.0 and 2.1. * <p> * This event is supported on any {@link UIComponent}, including {@link UIViewRoot}, {@link UIForm}, {@link UIInput} and * {@link UICommand} components. This thus also provides the possibility to invoke multiple action listeners on a single * {@link UIInput} and {@link UICommand} component on an easy manner. * <p> * As this phase listener has totally no impact on a webapp's default behavior, this phase listener is already * registered by OmniFaces own <code>faces-config.xml</code> and thus gets auto-initialized when the OmniFaces JAR * is bundled in a webapp, so endusers do not need to register this phase listener explicitly themselves. * * @author Bauke Scholtz * @see PreInvokeActionEvent * @see PostInvokeActionEvent * @since 1.1 */ public class InvokeActionEventListener extends DefaultPhaseListener implements SystemEventListener { // Constants ------------------------------------------------------------------------------------------------------ private static final long serialVersionUID = -7324254442944700095L; // Constructors --------------------------------------------------------------------------------------------------- /** * This constructor instructs the {@link DefaultPhaseListener} to hook on {@link PhaseId#INVOKE_APPLICATION} and * subscribes this instance as a {@link SystemEventListener} to the {@link PostValidateEvent} event. This allows * collecting the components eligible for {@link PreInvokeActionEvent} or {@link PostInvokeActionEvent} inside the * {@link #processEvent(SystemEvent)} method. */ public InvokeActionEventListener() { super(PhaseId.INVOKE_APPLICATION); subscribeToApplicationEvent(PostValidateEvent.class, this); } // Actions -------------------------------------------------------------------------------------------------------- /** * Returns <code>true</code> only when the given source is an {@link UIComponent} which has listeners for * {@link PreInvokeActionEvent} or {@link PostInvokeActionEvent}. */ @Override public boolean isListenerForSource(Object source) { if (!(source instanceof UIComponent)) { return false; } UIComponent component = (UIComponent) source; return !isEmpty(component.getListenersForEventClass(PreInvokeActionEvent.class)) || !isEmpty(component.getListenersForEventClass(PostInvokeActionEvent.class)); } /** * If the validation has not failed for the current faces context, then check if the {@link UIComponent} which * passed the {@link #isListenerForSource(Object)} check has any listeners for the {@link PreInvokeActionEvent} * and/or {@link PostInvokeActionEvent} events and then add them to a set in the current faces context. */ @Override public void processEvent(SystemEvent event) { FacesContext context = FacesContext.getCurrentInstance(); if (!context.isValidationFailed()) { UIComponent component = (UIComponent) event.getSource(); checkAndAddComponentWithListeners(context, component, PreInvokeActionEvent.class); checkAndAddComponentWithListeners(context, component, PostInvokeActionEvent.class); } } /** * Publish the {@link PreInvokeActionEvent} event on the components which are been collected in * {@link #processEvent(SystemEvent)}. */ @Override public void beforePhase(PhaseEvent event) { publishEvent(event.getFacesContext(), PreInvokeActionEvent.class); } /** * Publish the {@link PostInvokeActionEvent} event on the components which are been collected in * {@link #processEvent(SystemEvent)}. */ @Override public void afterPhase(PhaseEvent event) { publishEvent(event.getFacesContext(), PostInvokeActionEvent.class); } // Helpers -------------------------------------------------------------------------------------------------------- /** * If {@link UIComponent#getListenersForEventClass(Class)} returns a non-<code>null</code> and non-empty collection, * then add the component to the set of components associated with the given event type. * @param context The involved faces context. * @param component The component to be checked. * @param type The event type. */ @SuppressWarnings("unchecked") // For the cast on Set<UIComponent>. private static <T extends SystemEvent> void checkAndAddComponentWithListeners (FacesContext context, UIComponent component, Class<T> type) { if (!isEmpty(component.getListenersForEventClass(type))) { Set<UIComponent> components = (Set<UIComponent>) context.getAttributes().get(type); if (components == null) { components = new LinkedHashSet<>(); context.getAttributes().put(type, components); } components.add(component); } } /** * Obtain the set of components associated with the given event type and publish the event on each of them. * @param context The involved faces context. * @param type The event type. */ @SuppressWarnings("unchecked") // For the cast on Set<UIComponent>. private static <T extends SystemEvent> void publishEvent(FacesContext context, Class<T> type) { Set<UIComponent> components = (Set<UIComponent>) context.getAttributes().get(type); if (components != null) { for (UIComponent component : components) { context.getApplication().publishEvent(context, type, component); } } } }