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 };
}
}