/* * JBoss, Home of Professional Open Source * * Distributable under LGPL license. * See terms of license at gnu.org. */ package org.jboss.seam.core; import static org.jboss.seam.annotations.Install.BUILT_IN; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import org.jboss.seam.Component; import org.jboss.seam.ConcurrentRequestTimeoutException; import org.jboss.seam.ScopeType; import org.jboss.seam.annotations.FlushModeType; import org.jboss.seam.annotations.Install; import org.jboss.seam.annotations.Name; import org.jboss.seam.annotations.Scope; import org.jboss.seam.annotations.intercept.BypassInterceptors; import org.jboss.seam.contexts.Contexts; import org.jboss.seam.contexts.Lifecycle; import org.jboss.seam.log.LogProvider; import org.jboss.seam.log.Logging; import org.jboss.seam.navigation.ConversationIdParameter; import org.jboss.seam.navigation.Pages; import org.jboss.seam.pageflow.Pageflow; import org.jboss.seam.util.Id; import org.jboss.seam.web.Session; /** * The Seam conversation manager. * * @author Gavin King * @author <a href="mailto:theute@jboss.org">Thomas Heute</a> */ @Scope(ScopeType.EVENT) @Name("org.jboss.seam.core.manager") @Install(precedence=BUILT_IN) @BypassInterceptors public class Manager { public static final String EVENT_CONVERSATION_TIMEOUT = "org.jboss.seam.conversationTimeout"; public static final String EVENT_CONVERSATION_DESTROYED = "org.jboss.seam.conversationDestroyed"; public static final String EVENT_CONVERSATION_BEGIN = "org.jboss.seam.beginConversation"; public static final String EVENT_CONVERSATION_END = "org.jboss.seam.endConversation"; private static final LogProvider log = Logging.getLogProvider(Manager.class); public static final String REDIRECT_FROM_MANAGER = "org.jboss.seam.core.Manager"; private static final String DEFAULT_ENCODING = "UTF-8"; //The id of the current conversation private String currentConversationId; private List<String> currentConversationIdStack; //Is the current conversation "long-running"? private boolean isLongRunningConversation; //private boolean updateModelValuesCalled; private boolean destroyBeforeRedirect; private int conversationTimeout = 600000; //10 mins private int concurrentRequestTimeout = 1000; //one second private String conversationIdParameter = "conversationId"; private String parentConversationIdParameter = "parentConversationId"; private String URIEncoding = DEFAULT_ENCODING; private FlushModeType defaultFlushMode; /** * Kills all conversations except the current one */ public void killAllOtherConversations() { ConversationEntries conversationEntries = ConversationEntries.instance(); Events events = Events.exists() ? Events.instance() : null; if (conversationEntries != null) { List<ConversationEntry> entries = new ArrayList<ConversationEntry>( conversationEntries.getConversationEntries()); for (ConversationEntry conversationEntry : entries) { // kill all entries expect the current one // current conversation entry will be null if , kill-all is called // inside a new @Begin if (getCurrentConversationEntry() == null || !getCurrentConversationIdStack().contains( conversationEntry.getId())) { log.debug("Kill all other conversations, executed: kill conversation id = " + conversationEntry.getId()); boolean locked = conversationEntry.lockNoWait(); // we had better // not wait for it, or we would be waiting for ALL other requests try { if (locked) { if (log.isDebugEnabled()) { log.debug("conversation killed manually: " + conversationEntry.getId()); } } else { // if we could not acquire the lock, someone has left a // garbage lock lying around // the reason garbage locks can exist is that we don't // require a servlet filter to // exist - but if we do use SeamExceptionFilter, it will // clean up garbage and this // case should never occur // NOTE: this is slightly broken - in theory there is a // window where a new request // could have come in and got the lock just before us but // called touch() just // after we check the timeout - but in practice this would // be extremely rare, // and that request will get an // IllegalMonitorStateException when it tries to // unlock() the CE log.debug("kill conversation with garbage lock: " + conversationEntry.getId()); } if (events != null) { events.raiseEvent(EVENT_CONVERSATION_DESTROYED, conversationEntry); } destroyConversation(conversationEntry.getId(), getSessionMap()); } finally { if (locked) { conversationEntry.unlock(); } } } } } } /** * @return Map session */ private Map<String, Object> getSessionMap() { // this method could be moved to a utility class Map<String, Object> session = new HashMap<String, Object>(); String[] sessionAttributeNames = Contexts.getSessionContext().getNames(); for (String attributeName : sessionAttributeNames) { session.put(attributeName, Contexts.getSessionContext().get(attributeName)); } return session; } // DONT BREAK, icefaces uses this public String getCurrentConversationId() { return currentConversationId; } /** * Only public for the unit tests! * @param id */ public void setCurrentConversationId(String id) { currentConversationId = id; currentConversationEntry = null; } /** * Change the id of the current conversation. * * @param id the new conversation id */ public void updateCurrentConversationId(String id) { if (id != null && id.equals(currentConversationId)) { // the conversation id hasn't changed, do nothing return; } if ( ConversationEntries.instance().getConversationIds().contains(id) ) { throw new IllegalStateException("Conversation id is already in use: " + id); } String[] names = Contexts.getConversationContext().getNames(); Object[] values = new Object[names.length]; for (int i=0; i<names.length; i++) { values[i] = Contexts.getConversationContext().get(names[i]); Contexts.getConversationContext().remove(names[i]); } Contexts.getConversationContext().flush(); ConversationEntry ce = ConversationEntries.instance().updateConversationId(currentConversationId, id); String priorId = currentConversationId; setCurrentConversationId(id); if (ce!=null) { setCurrentConversationIdStack( ce.getConversationIdStack() ); //TODO: what about child conversations?! } else { // when ce is null, the id stack will be left with a reference to // the old conversation id, so we need patch that up int pos = currentConversationIdStack.indexOf(priorId); if (pos != -1) { currentConversationIdStack.set(pos, id); } } for (int i=0; i<names.length; i++) { Contexts.getConversationContext().set(names[i], values[i]); } } private void touchConversationStack(List<String> stack) { if ( stack!=null ) { //iterate in reverse order, so that current conversation //sits at top of conversation lists ListIterator<String> iter = stack.listIterator( stack.size() ); while ( iter.hasPrevious() ) { String conversationId = iter.previous(); ConversationEntry conversationEntry = ConversationEntries.instance().getConversationEntry(conversationId); if (conversationEntry!=null) { conversationEntry.touch(); } } } } private void endNestedConversations(String id) { for ( ConversationEntry ce: ConversationEntries.instance().getConversationEntries() ) { if ( ce.getConversationIdStack().contains(id) ) { ce.end(); } } } public List<String> getCurrentConversationIdStack() { return currentConversationIdStack; } public void setCurrentConversationIdStack(List<String> stack) { currentConversationIdStack = stack; } private List<String> createCurrentConversationIdStack(String id) { currentConversationIdStack = new ArrayList<String>(); currentConversationIdStack.add(id); return currentConversationIdStack; } public String getCurrentConversationDescription() { ConversationEntry ce = getCurrentConversationEntry(); if ( ce==null ) return null; return ce.getDescription(); } public Integer getCurrentConversationTimeout() { ConversationEntry ce = getCurrentConversationEntry(); if ( ce==null ) return null; return ce.getTimeout(); } public Integer getCurrentConversationConcurrentRequestTimeout() { ConversationEntry ce = getCurrentConversationEntry(); if (ce == null) return null; return ce.getConcurrentRequestTimeout(); } public String getCurrentConversationViewId() { ConversationEntry ce = getCurrentConversationEntry(); if ( ce==null ) return null; return ce.getViewId(); } public String getParentConversationViewId() { ConversationEntry conversationEntry = ConversationEntries.instance().getConversationEntry(getParentConversationId()); return conversationEntry==null ? null : conversationEntry.getViewId(); } public String getParentConversationId() { return currentConversationIdStack==null || currentConversationIdStack.size()<2 ? null : currentConversationIdStack.get(1); } public String getRootConversationId() { return currentConversationIdStack==null || currentConversationIdStack.size()<1 ? null : currentConversationIdStack.get( currentConversationIdStack.size()-1 ); } // DONT BREAK, icefaces uses this public boolean isLongRunningConversation() { return isLongRunningConversation; } public boolean isLongRunningOrNestedConversation() { return isLongRunningConversation() || isNestedConversation(); } public boolean isReallyLongRunningConversation() { return isLongRunningConversation() && !getCurrentConversationEntry().isRemoveAfterRedirect() && !Session.instance().isInvalid(); } public boolean isNestedConversation() { return currentConversationIdStack!=null && currentConversationIdStack.size()>1; } public void setLongRunningConversation(boolean isLongRunningConversation) { this.isLongRunningConversation = isLongRunningConversation; } public static Manager instance() { if ( !Contexts.isEventContextActive() ) { throw new IllegalStateException("No active event context"); } Manager instance = (Manager) Component.getInstance(Manager.class, ScopeType.EVENT); if (instance==null) { throw new IllegalStateException("No Manager could be created, make sure the Component exists in application scope"); } return instance; } /** * Clean up timed-out conversations */ public void conversationTimeout(Map<String, Object> session) { long currentTime = System.currentTimeMillis(); ConversationEntries conversationEntries = ConversationEntries.getInstance(); if (conversationEntries!=null) { List<ConversationEntry> entries = new ArrayList<ConversationEntry>( conversationEntries.getConversationEntries() ); for (ConversationEntry conversationEntry: entries) { boolean locked = conversationEntry.lockNoWait(); //we had better not wait for it, or we would be waiting for ALL other requests try { long delta = currentTime - conversationEntry.getLastRequestTime(); if ( delta > conversationEntry.getTimeout() ) { if ( locked ) { if ( log.isDebugEnabled() ) { log.debug("conversation timeout for conversation: " + conversationEntry.getId()); } } else { //if we could not acquire the lock, someone has left a garbage lock lying around //the reason garbage locks can exist is that we don't require a servlet filter to //exist - but if we do use SeamExceptionFilter, it will clean up garbage and this //case should never occur //NOTE: this is slightly broken - in theory there is a window where a new request // could have come in and got the lock just before us but called touch() just // after we check the timeout - but in practice this would be extremely rare, // and that request will get an IllegalMonitorStateException when it tries to // unlock() the CE log.debug("destroying conversation with garbage lock: " + conversationEntry.getId()); } if ( Events.exists() ) { Events.instance().raiseEvent(EVENT_CONVERSATION_TIMEOUT, conversationEntry.getId()); } destroyConversation( conversationEntry.getId(), session ); } } finally { if (locked) conversationEntry.unlock(); } } } } /** * Clean up all state associated with a conversation */ private void destroyConversation(String conversationId, Map<String, Object> session) { Lifecycle.destroyConversationContext(session, conversationId); ConversationEntries.instance().removeConversationEntry(conversationId); } /** * Touch the conversation stack, destroy ended conversations, * and timeout inactive conversations. */ public void endRequest(Map<String, Object> session) { if ( isLongRunningConversation() ) { if ( log.isDebugEnabled() ) { log.debug("Storing conversation state: " + getCurrentConversationId()); } touchConversationStack( getCurrentConversationIdStack() ); } else { if ( log.isDebugEnabled() ) { log.debug("Discarding conversation state: " + getCurrentConversationId()); } //now safe to remove the entry removeCurrentConversationAndDestroyNestedContexts(session); } /*if ( !Init.instance().isClientSideConversations() ) {*/ // difficult question: is it really safe to do this here? // right now we do have to do it after committing the Seam // transaction because we can't close EMs inside a txn // (this might be a bug in HEM) Manager.instance().conversationTimeout(session); //} } public void unlockConversation() { ConversationEntry ce = getCurrentConversationEntry(); if (ce!=null) { if ( ce.isLockedByCurrentThread() ) { ce.unlock(); } } else if ( isNestedConversation() ) { ConversationEntries.instance().getConversationEntry( getParentConversationId() ).unlock(); } } private void removeCurrentConversationAndDestroyNestedContexts(Map<String, Object> session) { ConversationEntries conversationEntries = ConversationEntries.getInstance(); if (conversationEntries!=null) { conversationEntries.removeConversationEntry( getCurrentConversationId() ); destroyNestedConversationContexts( session, getCurrentConversationId() ); } } private void destroyNestedConversationContexts(Map<String, Object> session, String conversationId) { List<ConversationEntry> entries = new ArrayList<ConversationEntry>( ConversationEntries.instance().getConversationEntries() ); for ( ConversationEntry ce: entries ) { if ( ce.getConversationIdStack().contains(conversationId) ) { String entryConversationId = ce.getId(); log.debug("destroying nested conversation: " + entryConversationId); destroyConversation(entryConversationId, session); } } } /** * Look for a conversation propagation style in the request * parameters and begin, nested or join the conversation, * as necessary. * * @param parameters the request parameters */ public void handleConversationPropagation(Map parameters) { ConversationPropagation propagation = ConversationPropagation.instance(); if (propagation.getPropagationType() == null) { return; } switch (propagation.getPropagationType()) { case BEGIN: if ( isLongRunningConversation ) { throw new IllegalStateException("long-running conversation already active"); } beginConversation(); if (propagation.getPageflow() != null) { Pageflow.instance().begin( propagation.getPageflow() ); } break; case JOIN: if ( !isLongRunningConversation ) { beginConversation(); if (propagation.getPageflow() != null) { Pageflow.instance().begin( propagation.getPageflow() ); } } break; case NESTED: if ( isLongRunningOrNestedConversation() ) { beginNestedConversation(); } else { beginConversation(); } if (propagation.getPageflow() != null) { Pageflow.instance().begin( propagation.getPageflow() ); } break; case END: endConversation(false); break; case ENDROOT: endRootConversation(false); break; } } /** * Initialize the request conversation context, given the * conversation id and optionally a parent conversation id. * If no conversation entry is found for the first id, try * the parent, and if that also fails, initialize a new * temporary conversation context. * * @return false if the conversation entry was not found * and it was required */ public boolean restoreConversation() { ConversationPropagation cp = ConversationPropagation.instance(); String conversationId = cp.getConversationId(); String parentConversationId = cp.getParentConversationId(); ConversationEntry ce = null; if (conversationId!=null) { ConversationEntries entries = ConversationEntries.instance(); ce = entries.getConversationEntry(conversationId); if (ce==null) { ce = entries.getConversationEntry(parentConversationId); } } return restoreAndLockConversation(ce) || !cp.isValidateLongRunningConversation(); } private boolean restoreAndLockConversation(ConversationEntry ce) { if (ce == null) { //there was no id in either place, so there is no //long-running conversation to restore log.debug("No stored conversation"); initializeTemporaryConversation(); return false; } else if ( ce.lock() ) { // do this ASAP, since there is a window where conversationTimeout() might // try to destroy the conversation, even if he cannot obtain the lock! touchConversationStack( ce.getConversationIdStack() ); //we found an id and obtained the lock, so restore the long-running conversation log.debug("Restoring conversation with id: " + ce.getId()); setLongRunningConversation(true); setCurrentConversationId( ce.getId() ); setCurrentConversationIdStack( ce.getConversationIdStack() ); boolean removeAfterRedirect = ce.isRemoveAfterRedirect() && !Pages.isDebugPage(); //TODO: hard dependency to JSF!! if (removeAfterRedirect) { setLongRunningConversation(false); ce.setRemoveAfterRedirect(false); } return true; } else { log.debug("Concurrent call to conversation"); throw new ConcurrentRequestTimeoutException("Concurrent call to conversation"); } } /** * Initialize a new temporary conversation context, * and assign it a conversation id. */ public void initializeTemporaryConversation() { String id = generateInitialConversationId(); setCurrentConversationId(id); createCurrentConversationIdStack(id); setLongRunningConversation(false); } protected String generateInitialConversationId() { return Id.nextId(); } private ConversationEntry createConversationEntry() { ConversationEntry entry = ConversationEntries.instance() .createConversationEntry( getCurrentConversationId(), getCurrentConversationIdStack() ); if ( !entry.isNested() ) { //if it is a newly created nested //conversation, we already own the //lock entry.lock(); } return entry; } /** * Promote a temporary conversation and make it long-running */ public void beginConversation() { if ( !isLongRunningConversation() ) { log.debug("Beginning long-running conversation"); setLongRunningConversation(true); createConversationEntry(); Conversation.instance(); //force instantiation of the Conversation in the outer (non-nested) conversation storeConversationToViewRootIfNecessary(); if ( Events.exists() ) Events.instance().raiseEvent(EVENT_CONVERSATION_BEGIN); } } /** * Begin a new nested conversation. */ public void beginNestedConversation() { log.debug("Beginning nested conversation"); List<String> oldStack = getCurrentConversationIdStack(); if (oldStack==null) { throw new IllegalStateException("No long-running conversation active"); } String id = Id.nextId(); setCurrentConversationId(id); createCurrentConversationIdStack(id).addAll(oldStack); createConversationEntry(); storeConversationToViewRootIfNecessary(); if ( Events.exists() ) Events.instance().raiseEvent(EVENT_CONVERSATION_BEGIN); } /** * Make a long-running conversation temporary. */ public void endConversation(boolean beforeRedirect) { if ( isLongRunningConversation() ) { log.debug("Ending long-running conversation"); if ( Events.exists() ) Events.instance().raiseEvent(EVENT_CONVERSATION_END); setLongRunningConversation(false); destroyBeforeRedirect = beforeRedirect; endNestedConversations( getCurrentConversationId() ); storeConversationToViewRootIfNecessary(); } } /** * Make the root conversation in the current conversation stack temporary. */ public void endRootConversation(boolean beforeRedirect) { if(isNestedConversation()) { switchConversation(getRootConversationId()); } endConversation(beforeRedirect); } protected void storeConversationToViewRootIfNecessary() {} // two reasons for this: // (1) a cache // (2) so we can unlock() it after destruction of the session context private ConversationEntry currentConversationEntry; public ConversationEntry getCurrentConversationEntry() { if (currentConversationEntry==null) { currentConversationEntry = ConversationEntries.instance().getConversationEntry( getCurrentConversationId() ); } return currentConversationEntry; } /** * Leave the scope of the current conversation, leaving * it completely intact. */ public void leaveConversation() { unlockConversation(); initializeTemporaryConversation(); } /** * Switch to another long-running conversation and mark the conversation as long-running, * overriding a previous call in the same thread to demote a long-running conversation. * * @param id the id of the conversation to switch to * @return true if the conversation exists */ public boolean switchConversation(String id) { return switchConversation(id, true); } /** * Switch to another long-running conversation. * * @param id the id of the conversation to switch to * @param promote promote the current conversation to long-running, overriding any previous demotion * @return true if the conversation exists */ public boolean switchConversation(String id, boolean promote) { ConversationEntry ce = ConversationEntries.instance().getConversationEntry(id); if (ce!=null) { if ( ce.lock() ) { unlockConversation(); setCurrentConversationId(id); setCurrentConversationIdStack( ce.getConversationIdStack() ); if (promote) { setLongRunningConversation(true); } return true; } else { return false; } } else { return false; } } public int getConversationTimeout() { return conversationTimeout; } public void setConversationTimeout(int conversationTimeout) { this.conversationTimeout = conversationTimeout; } /** * Temporarily promote a temporary conversation to * a long running conversation for the duration of * a browser redirect. After the redirect, the * conversation will be demoted back to a temporary * conversation. */ public void beforeRedirect() { //DONT BREAK, icefaces uses this if (!destroyBeforeRedirect) { ConversationEntry ce = getCurrentConversationEntry(); if (ce==null) { ce = createConversationEntry(); } //ups, we don't really want to destroy it on this request after all! ce.setRemoveAfterRedirect( !isLongRunningConversation() ); setLongRunningConversation(true); } } protected static boolean isDifferentConversationId(ConversationIdParameter sp, ConversationIdParameter tp) { return sp.getName()!=tp.getName() && ( sp.getName()==null || !sp.getName().equals( tp.getName() ) ); } /** * Add the conversation id to a URL, if necessary * * @deprecated use encodeConversationId(String url, String viewId) */ public String encodeConversationId(String url) { //DONT BREAK, icefaces uses this return encodeConversationIdParameter( url, getConversationIdParameter(), getCurrentConversationId() ); } /** * Add the conversation id to a URL, if necessary */ public String encodeConversationId(String url, String viewId) { //DONT BREAK, icefaces uses this ConversationIdParameter cip = Pages.instance().getPage(viewId).getConversationIdParameter(); return encodeConversationIdParameter( url, cip.getParameterName(), cip.getParameterValue() ); } /** * Add the conversation id to a URL, if necessary */ public String encodeConversationId(String url, String viewId, String conversationId) { //DONT BREAK, icefaces uses this ConversationIdParameter cip = Pages.instance().getPage(viewId).getConversationIdParameter(); return encodeConversationIdParameter( url, cip.getParameterName(), cip.getParameterValue(conversationId) ); } protected String encodeConversationIdParameter(String url, String paramName, String paramValue) { if ( Session.instance().isInvalid() || containsParameter(url, paramName) ) { return url; } else if (destroyBeforeRedirect) { if ( isNestedConversation() ) { return new StringBuilder( url.length() + paramName.length() + 5 ) .append(url) .append( url.contains("?") ? '&' : '?' ) .append(paramName) .append('=') .append( encode( getParentConversationId() ) ) .toString(); } else { return url; } } else { StringBuilder builder = new StringBuilder( url.length() + paramName.length() + 5 ) .append(url) .append( url.contains("?") ? '&' : '?' ) .append(paramName) .append('=') .append( encode(paramValue) ); if ( isNestedConversation() && !isReallyLongRunningConversation() ) { builder.append('&') .append(parentConversationIdParameter) .append('=') .append( encode( getParentConversationId() ) ); } return builder.toString(); } } /** * Add the parameters to a URL */ public String encodeParameters(String url, Map<String, Object> parameters) { if ( parameters.isEmpty() ) return url; StringBuilder builder = new StringBuilder(url); for ( Map.Entry<String, Object> param: parameters.entrySet() ) { String parameterName = param.getKey(); if ( !containsParameter(url, parameterName) ) { Object parameterValue = param.getValue(); if (parameterValue instanceof Iterable) { for ( Object value: (Iterable) parameterValue ) { builder.append('&') .append(parameterName) .append('='); if (value!=null) { builder.append(encode(value)); } } } else { builder.append('&') .append(parameterName) .append('='); if (parameterValue!=null) { builder.append(encode(parameterValue)); } } } } if ( url.indexOf('?')<0 ) { builder.setCharAt( url.length() ,'?' ); } return builder.toString(); } private boolean containsParameter(String url, String parameterName) { return url.indexOf('?' + parameterName + '=')>0 || url.indexOf( '&' + parameterName + '=')>0; } private String encode(Object value) { try { return URLEncoder.encode(String.valueOf(value),getUriEncoding()); } catch (UnsupportedEncodingException iee) { throw new RuntimeException(iee); } } public String getConversationIdParameter() { return conversationIdParameter; } public void setConversationIdParameter(String conversationIdParameter) { this.conversationIdParameter = conversationIdParameter; } public String getParentConversationIdParameter() { return parentConversationIdParameter; } public void setParentConversationIdParameter(String nestedConversationIdParameter) { this.parentConversationIdParameter = nestedConversationIdParameter; } public int getConcurrentRequestTimeout() { return concurrentRequestTimeout; } public void setConcurrentRequestTimeout(int requestWait) { this.concurrentRequestTimeout = requestWait; } public FlushModeType getDefaultFlushMode() { return defaultFlushMode; } public void setDefaultFlushMode(FlushModeType defaultFlushMode) { this.defaultFlushMode = defaultFlushMode; } @Override public String toString() { return "Manager(" + currentConversationIdStack + ")"; } public void redirect(String viewId, String id) { //declare it here since ConversationEntry calls it! throw new UnsupportedOperationException(); } public void redirect(String viewId) { //declare it here since Conversation calls it! throw new UnsupportedOperationException(); } protected void flushConversationMetadata() { if ( isLongRunningConversation() ) { //important: only do this stuff when a long-running // conversation exists, otherwise we would // force creation of a conversation entry Conversation.instance().flush(); } } public String getUriEncoding() { return URIEncoding; } public void setUriEncoding(String encoding) { URIEncoding = encoding; } }