package com.icefaces.taglib; import java.io.IOException; import java.util.ArrayList; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.el.ELException; import javax.el.VariableMapper; import javax.faces.FacesException; import javax.faces.component.UIComponent; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.binding.expression.Expression; import org.springframework.context.ApplicationContext; import org.springframework.faces.webflow.FlowExecutionHolder; import org.springframework.faces.webflow.FlowExecutionHolderUtils; import org.springframework.faces.webflow.FlowFacesUtils; import org.springframework.faces.webflow.JsfExternalContext; import org.springframework.util.StringUtils; import org.springframework.web.jsf.FacesContextUtils; import org.springframework.web.servlet.ModelAndView; import org.springframework.webflow.context.ExternalContextHolder; import org.springframework.webflow.engine.Flow; import org.springframework.webflow.engine.State; import org.springframework.webflow.engine.SubflowState; import org.springframework.webflow.engine.ViewState; import org.springframework.webflow.engine.impl.FlowExecutionImpl; import org.springframework.webflow.engine.support.ApplicationViewSelector; import org.springframework.webflow.execution.FlowExecution; import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.repository.BadlyFormattedFlowExecutionKeyException; import org.springframework.webflow.execution.repository.FlowExecutionKey; import org.springframework.webflow.execution.repository.FlowExecutionLock; import org.springframework.webflow.execution.repository.FlowExecutionRepository; import org.springframework.webflow.executor.mvc.FlowController; import com.icesoft.faces.component.paneltabset.PanelTab; import com.icesoft.faces.component.paneltabset.PanelTabSet; import com.icesoft.faces.env.ServletEnvironmentRequest; import com.icesoft.faces.webapp.http.servlet.ServletExternalContext; import com.sun.facelets.FaceletContext; import com.sun.facelets.FaceletException; import com.sun.facelets.el.VariableMapperWrapper; import com.sun.facelets.tag.TagAttribute; import com.sun.facelets.tag.TagConfig; import com.sun.facelets.tag.TagHandler; public class WebflowIncludeHandler extends TagHandler { /** Logger that is available to subclasses */ protected final Log logger = LogFactory.getLog(getClass()); private final static String MAINTAINED_FLOWS = "maintainedFlows"; private final static String TAB_HIERARCHY_PARAM = "tabHierarchy"; private final TagAttribute flowControllerAttr; private final TagAttribute tabHierarchyAttr; private final TagAttribute resetOnTabChange; /** * @param config */ public WebflowIncludeHandler(TagConfig config) { super(config); this.flowControllerAttr = this.getRequiredAttribute("flowController"); this.tabHierarchyAttr = this.getRequiredAttribute("tabHierarchy"); this.resetOnTabChange = this.getAttribute("resetOnTabChange"); } /* * (non-Javadoc) * * @see com.sun.facelets.FaceletHandler#apply(com.sun.facelets.FaceletContext, * javax.faces.component.UIComponent) */ public void apply(FaceletContext ctx, UIComponent parent) throws IOException, FacesException, FaceletException, ELException { VariableMapper orig = ctx.getVariableMapper(); ctx.setVariableMapper(new VariableMapperWrapper(orig)); try { this.nextHandler.apply(ctx, null); FacesContext facesCtx = ctx.getFacesContext(); JsfExternalContext webFlowExtCtx = new JsfExternalContext(facesCtx); String tabHierarchy = (String) tabHierarchyAttr.getValue(ctx); FlowExecutionRepository repository = FlowFacesUtils.getExecutionRepository(facesCtx); ExternalContext eCtx = facesCtx.getExternalContext(); Map requestParameterMap = ((ServletEnvironmentRequest) eCtx.getRequest()).getParameterMap(); for (Iterator iterator = eCtx.getRequestParameterNames(); iterator.hasNext();) { String paramName = (String) iterator.next(); requestParameterMap.put(paramName, eCtx.getRequestParameterValuesMap().get(paramName)); } // if flow isn't started yet then start it // otherwise assume webflow by default will handle transition // our job here then would be just to include the page FlowController controller = getFlowController(ctx); String flowId = controller.getArgumentHandler().getDefaultFlowId(); FlowExecutionHolder flowExecutionHolder = FlowExecutionHolderUtils.getFlowExecutionHolder(facesCtx); // we are not in executing flow or // the panel we are displaying is visible and // the flow currently executing does not match the // the flow we want to display then end the current flow and start a new flow PanelTabSet tabs = (PanelTabSet) parent.getParent(); boolean isFlowRendered = false; String[] tabHierarchyParamArr = (String[]) requestParameterMap.get(TAB_HIERARCHY_PARAM); if (tabs == null) { logger.info("PanelTabSet is null"); String paramTabHierarchy = "0"; if (tabHierarchyParamArr != null) { String tabIndex[] = StringUtils.commaDelimitedListToStringArray(tabHierarchyParamArr[0]); List paramTabHierarchyList = new ArrayList(); for (int i = 0; i < tabIndex.length; i++) { String requestParamName = tabIndex[i]; paramTabHierarchyList.add(((String[]) requestParameterMap.get(requestParamName))[0]); } paramTabHierarchy = StringUtils.collectionToCommaDelimitedString(paramTabHierarchyList); } isFlowRendered = tabHierarchy.equals(paramTabHierarchy); // if we are on very first page } else { logger.info("PanelTabSet is not null"); // if the tab is selected and we are on that tab // because of a bug in the ordering of children vs selected index // this is not easy to do // work araround: // traverse parents to verify all correct tabs are selected if (tabHierarchyAttr != null) { String tabIndex[] = StringUtils.commaDelimitedListToStringArray(tabHierarchy); int tabIdx = 0; UIComponent currentComponent = parent; // this would be code if there wasn't a bug with child order //PanelTab previousPanelTab = null; while (currentComponent != null) { if (currentComponent instanceof PanelTab) { // this would be code if there wasn't a bug with child order //previousPanelTab = (PanelTab) currentComponent; } if (currentComponent instanceof PanelTabSet) { int selectedIndex = ((PanelTabSet) currentComponent).getSelectedIndex(); // this would be code if there wasn't a bug with child order //if (((PanelTabSet) currentComponent).getChildren().get(selectedIndex) != previousPanelTab) { if (Integer.valueOf(tabIndex[tabIdx]).intValue() != selectedIndex) { isFlowRendered = false; break; } else { isFlowRendered = true; // this would be code if there wasn't a bug with child order // add selected tab to tab hierarchy } tabIdx++; } currentComponent = currentComponent.getParent(); } } // clear out request params in this case if (tabHierarchyParamArr != null) { requestParameterMap.remove(TAB_HIERARCHY_PARAM); } } // if there is a flow execution key and there is no flow in repository with that key then // remove that key from request params so new flow can be started Map maintainedFlows = (Map) eCtx.getSessionMap().get(MAINTAINED_FLOWS); if (maintainedFlows == null) { maintainedFlows = new Hashtable(); eCtx.getSessionMap().put(MAINTAINED_FLOWS,maintainedFlows); } // if flowExecutionHolder is not null then capture the continuationId change and // update any maintained flows. if (flowExecutionHolder != null) { String newFlowExecutionKeyStr = flowExecutionHolder.getFlowExecutionKey().toString(); String conversationId = keyParts(flowExecutionHolder.getFlowExecutionKey().toString())[0]; logger.info("new FlowExecution Key:" + newFlowExecutionKeyStr); logger.info("conversation Id:" + conversationId); for (Iterator iterator = maintainedFlows.keySet().iterator(); iterator .hasNext();) { String key = (String) iterator.next(); String previousFlowExecutionKeyStr = (String) maintainedFlows.get(key); if (previousFlowExecutionKeyStr != null && previousFlowExecutionKeyStr.indexOf(CONVERSATION_ID_PREFIX + conversationId) != -1) { if (! previousFlowExecutionKeyStr.equals(newFlowExecutionKeyStr)) { try { ExternalContextHolder.setExternalContext(webFlowExtCtx); FlowExecutionLock lock = flowExecutionHolder.getFlowExecutionLock(); try { logger.info("before lock1"); lock.lock(); logger.info("after lock1"); maintainedFlows.put(key, newFlowExecutionKeyStr); repository.putFlowExecution(flowExecutionHolder.getFlowExecutionKey(), flowExecutionHolder.getFlowExecution()); break; } finally { lock.unlock(); } } finally { ExternalContextHolder.setExternalContext(null); } } } } } String currentTabFlowExecutionKeyStr = null; if (tabHierarchy != null) { currentTabFlowExecutionKeyStr = (String) maintainedFlows.get(tabHierarchy); } if (! isFlowRendered) { logger.info("not rendering " + flowId); // if the flow execution key and flow execution holder in // request map is ours then remove it. // When a tab is clicked on that does not contain a flow then when button is clicked // webflow infrastructure should not fire. // so by removing webflow infrastructure page will act like normal jsf page. String requestFlowExecutionKey = (String) eCtx.getRequestMap().get(controller.getArgumentHandler().getFlowExecutionKeyAttributeName()); if (requestFlowExecutionKey != null && requestFlowExecutionKey.equals(currentTabFlowExecutionKeyStr)) { eCtx.getRequestMap().remove(controller.getArgumentHandler().getFlowExecutionKeyAttributeName()); } FlowExecutionHolder requestFlowExecutionHolder = (FlowExecutionHolder) eCtx.getRequestMap().get(FlowExecutionHolder.class.getName()); if (requestFlowExecutionHolder != null && requestFlowExecutionHolder.getFlowExecutionKey().toString().equals(currentTabFlowExecutionKeyStr)) { eCtx.getRequestMap().remove(FlowExecutionHolder.class.getName()); } if (resetOnTabChange != null && resetOnTabChange.getBoolean(ctx)) { // restart the flow matching the flowId from flow execution repository if (currentTabFlowExecutionKeyStr != null) { FlowExecutionKey flowExecutionKey = repository.parseFlowExecutionKey(currentTabFlowExecutionKeyStr); try { ExternalContextHolder.setExternalContext(webFlowExtCtx); FlowExecutionLock lock = repository.getLock(flowExecutionKey); try { logger.info("before lock2"); lock.lock(); logger.info("after lock2"); FlowExecutionImpl flowExecution = (FlowExecutionImpl) repository.getFlowExecution(flowExecutionKey); if (flowExecution != null) { logger.info("tab change and reset so end this flow: "+flowId); flowExecution.endActiveFlowSession(); repository.removeFlowExecution(flowExecutionKey); maintainedFlows.remove(tabHierarchy); } } finally { lock.unlock(); } } finally { ExternalContextHolder.setExternalContext(null); } } } } else { // cases: // 1) if we switch flows then we need to clean out existing flow // 2) in the case there was no flow switch then webflow already handled the submit // so don't handle it with controller logger.info("rendering " + flowId); boolean noFlowSwitch = false; // if the flow is different then we need to switch to maintained flow or start new flow if (flowExecutionHolder != null) { try { ExternalContextHolder.setExternalContext(webFlowExtCtx); FlowExecutionLock flowExecutionLock = flowExecutionHolder.getFlowExecutionLock(); try { logger.info("before lock3"); flowExecutionLock.lock(); logger.info("after lock3"); FlowExecution flowExecution = FlowExecutionHolderUtils.getCurrentFlowExecution(facesCtx); if (! flowId.equals(flowExecution.getDefinition().getId())) { // clean up existing flow logger.info("cleaning up existing flow"); FlowExecutionHolderUtils.cleanupCurrentFlowExecution(facesCtx); logger.info("before lock4"); flowExecutionLock.lock(); logger.info("after lock4"); requestParameterMap.remove(controller.getArgumentHandler().getFlowExecutionKeyArgumentName()); if (currentTabFlowExecutionKeyStr != null) { requestParameterMap.put(controller.getArgumentHandler().getFlowExecutionKeyArgumentName(),new String[]{currentTabFlowExecutionKeyStr}); } eCtx.getRequestMap().clear(); } else { if (! flowExecution.isActive()) { logger.info("it's an old flow that's inactive"); requestParameterMap.remove(controller.getArgumentHandler().getFlowExecutionKeyArgumentName()); eCtx.getRequestMap().clear(); } else { logger.info("it's an existing flow"); noFlowSwitch = true; } } } finally { flowExecutionLock.unlock(); } } finally { ExternalContextHolder.setExternalContext(null); } } else { logger.info("flowExecutionHolder is null"); if (currentTabFlowExecutionKeyStr != null) { logger.info("currentTabFlowExecutionKeyStr: " + currentTabFlowExecutionKeyStr); requestParameterMap.put(controller.getArgumentHandler().getFlowExecutionKeyArgumentName(),new String[]{currentTabFlowExecutionKeyStr}); } else { requestParameterMap.remove(controller.getArgumentHandler().getFlowExecutionKeyArgumentName()); } } boolean newControllerRequest = false; if (! noFlowSwitch) { logger.info("executing new controller request"); ModelAndView mv = controller.handleRequest( (HttpServletRequest) eCtx.getRequest(), (HttpServletResponse) eCtx.getResponse()); exposeModelAsRequestAttributes(mv.getModel(), (HttpServletRequest) eCtx.getRequest()); ((ServletExternalContext) eCtx).updateOnReload( (HttpServletRequest) eCtx.getRequest(), (HttpServletResponse) eCtx.getResponse()); newControllerRequest = true; } // get new flowExecutionKey String newFlowExecutionKeyStr = (String) eCtx.getRequestMap().get(controller.getArgumentHandler().getFlowExecutionKeyAttributeName()); // can't render view without flowExecutionHolder so we need to make one if (FlowExecutionHolderUtils.getFlowExecutionHolder(facesCtx) == null ) { logger.info("flow execution holder not thread bound"); FlowExecutionKey flowExecutionKey = repository.parseFlowExecutionKey(newFlowExecutionKeyStr ); try { ExternalContextHolder.setExternalContext(webFlowExtCtx); FlowExecutionLock lock = repository.getLock(flowExecutionKey); try { logger.info("before lock5"); lock.lock(); logger.info("after lock5"); FlowExecution flowExecution = repository.getFlowExecution(flowExecutionKey); FlowExecutionHolderUtils.setFlowExecutionHolder(new FlowExecutionHolder(flowExecutionKey, flowExecution, lock), facesCtx); logger.info("storing flow execution holder"); } finally { lock.unlock(); } } finally { ExternalContextHolder.setExternalContext(null); } } logger.info("Storing in maintainedFlows:" + tabHierarchy + ":" + newFlowExecutionKeyStr); maintainedFlows.put(tabHierarchy,newFlowExecutionKeyStr ); if (newControllerRequest) { // go through all states and subflow states and replace view selectors with our own. Flow flow = (Flow) FlowExecutionHolderUtils.getFlowExecutionHolder(facesCtx).getFlowExecution().getActiveSession().getDefinition(); fixViewSelectors(flow); } ViewState currentViewState = (ViewState) FlowExecutionHolderUtils.getFlowExecutionHolder(facesCtx).getFlowExecution().getActiveSession().getState(); String viewName = ((ApplicationViewSelector) currentViewState.getViewSelector()).getViewName().toString(); logger.info("viewName:" + viewName); ctx.includeFacelet(parent, viewName); } } catch (Exception e) { throw new FacesException(e); } finally { ctx.setVariableMapper(orig); } } private void fixViewSelectors(Flow flow) { for (int i = 0; i < flow.getStateCount(); i++) { String stateId = flow.getStateIds()[i]; State state = (State) flow.getState(stateId); if (state instanceof ViewState) { ViewState viewState = (ViewState) state; ApplicationViewSelector stateViewSelector = (ApplicationViewSelector) viewState.getViewSelector(); viewState.setViewSelector(new TabViewSelector(stateViewSelector.getViewName(),stateViewSelector.isRedirect())); } if (state instanceof SubflowState) { SubflowState subflowState = (SubflowState) state; fixViewSelectors(subflowState.getSubflow()); } } } private class TabViewSelector extends ApplicationViewSelector { /** * */ private static final long serialVersionUID = -8380293729492929784L; public TabViewSelector(Expression viewName, boolean redirect) { super(viewName, redirect); } protected String resolveViewName(RequestContext context) { return FacesContext.getCurrentInstance().getViewRoot().getViewId(); } } private FlowController getFlowController(FaceletContext ctx) { ApplicationContext appCtx = FacesContextUtils.getRequiredWebApplicationContext(ctx.getFacesContext()); FlowController flowController = (FlowController) appCtx.getBean(flowControllerAttr.getValue(ctx)); return flowController; } /** * Expose the model objects in the given map as request attributes. * Names will be taken from the model Map. * This method is suitable for all resources reachable by {@link javax.servlet.RequestDispatcher}. * @param model Map of model objects to expose * @param request current HTTP request */ protected void exposeModelAsRequestAttributes(Map model, HttpServletRequest request) throws Exception { Iterator it = model.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); if (!(entry.getKey() instanceof String)) { throw new IllegalArgumentException( "Invalid key [" + entry.getKey() + "] in model Map: only Strings allowed as model keys"); } String modelName = (String) entry.getKey(); Object modelValue = entry.getValue(); if (modelValue != null) { request.setAttribute(modelName, modelValue); if (logger.isDebugEnabled()) { logger.debug("Added model object '" + modelName + "' of type [" + modelValue.getClass().getName() + "] to request in view with name '" + getBeanName() + "'"); } } else { request.removeAttribute(modelName); if (logger.isDebugEnabled()) { logger.debug("Removed model object '" + modelName + "' from request in view with name '" + getBeanName() + "'"); } } } } /** * Return the view's name. Should never be null, * if the view was correctly configured. */ public String getBeanName() { return ""; } /** * The default conversation id prefix delimiter ("_c"). */ private static final String CONVERSATION_ID_PREFIX = "_c"; /** * The default continuation id prefix delimiter ("_k"). */ private static final String CONTINUATION_ID_PREFIX = "_k"; /** * The format of the default string-encoded form, as returned * by toString(). */ private static final String FORMAT = CONVERSATION_ID_PREFIX + "" + CONTINUATION_ID_PREFIX + ""; private static String[] keyParts(String encodedKey) throws BadlyFormattedFlowExecutionKeyException { if (!encodedKey.startsWith(CONVERSATION_ID_PREFIX)) { throw new BadlyFormattedFlowExecutionKeyException(encodedKey, FORMAT); } int continuationStart = encodedKey.indexOf(CONTINUATION_ID_PREFIX, CONVERSATION_ID_PREFIX.length()); if (continuationStart == -1) { throw new BadlyFormattedFlowExecutionKeyException(encodedKey, FORMAT); } String conversationId = encodedKey.substring(CONVERSATION_ID_PREFIX.length(), continuationStart); String continuationId = encodedKey.substring(continuationStart + CONTINUATION_ID_PREFIX.length()); return new String[] { conversationId, continuationId }; } }