/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2013 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package org.nuxeo.ecm.platform.ui.web.application;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.io.IOException;
import javax.faces.FacesException;
import javax.faces.context.FacesContext;
import javax.faces.context.ExternalContext;
import javax.faces.context.ResponseWriter;
import javax.faces.component.UIViewRoot;
import com.sun.faces.util.TypedCollections;
import com.sun.faces.util.LRUMap;
import com.sun.faces.util.Util;
import com.sun.faces.util.RequestStateManager;
import static com.sun.faces.config.WebConfiguration.BooleanWebContextInitParameter.AutoCompleteOffOnViewState;
import static com.sun.faces.config.WebConfiguration.BooleanWebContextInitParameter.EnableViewStateIdRendering;
import javax.faces.render.ResponseStateManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.core.Conversation;
import com.sun.faces.renderkit.ServerSideStateHelper;
/**
* @since 6.0
*/
public class NuxeoServerSideStateHelper extends ServerSideStateHelper {
private static final Log log = LogFactory.getLog(NuxeoServerSideStateHelper.class);
public static final int DEFAULT_NUMBER_OF_CONVERSATIONS_IN_SESSION = 4;
public static final String NUMBER_OF_CONVERSATIONS_IN_SESSION = "nuxeo.jsf.numberOfConversationsInSession";
protected static final String NO_LONGRUNNING_CONVERSATION_ID = "NOLRC";
protected static Integer numbersOfConversationsInSession = null;
/**
* The top level attribute name for storing the state structures within the
* session.
*/
public static final String CONVERSATION_VIEW_MAP = NuxeoServerSideStateHelper.class.getName()
+ ".ConversationViewMap";
protected static int getNbOfConversationsInSession(FacesContext context) {
if (numbersOfConversationsInSession == null) {
ExternalContext externalContext = context.getExternalContext();
String value = externalContext.getInitParameter(NUMBER_OF_CONVERSATIONS_IN_SESSION);
if (null == value) {
numbersOfConversationsInSession = DEFAULT_NUMBER_OF_CONVERSATIONS_IN_SESSION;
} else {
try {
numbersOfConversationsInSession = Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new FacesException("Context parameter "
+ NUMBER_OF_CONVERSATIONS_IN_SESSION
+ " must have integer value");
}
}
}
return numbersOfConversationsInSession;
}
@Override
public void writeState(FacesContext ctx, Object state,
StringBuilder stateCapture) throws IOException {
Util.notNull("context", ctx);
String id;
if (!ctx.getViewRoot().isTransient()) {
Util.notNull("state", state);
Object[] stateToWrite = (Object[]) state;
ExternalContext externalContext = ctx.getExternalContext();
Object sessionObj = externalContext.getSession(true);
Map<String, Object> sessionMap = externalContext.getSessionMap();
// noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (sessionObj) {
String conversationId = NO_LONGRUNNING_CONVERSATION_ID;
if (Contexts.isConversationContextActive()
&& Conversation.instance().isLongRunning()) {
conversationId = Conversation.instance().getId();
}
Map<String, Map> conversationMap = TypedCollections.dynamicallyCastMap(
(Map) sessionMap.get(CONVERSATION_VIEW_MAP),
String.class, Map.class);
if (conversationMap == null) {
conversationMap = new LRUMap<String, Map>(
getNbOfConversationsInSession(ctx));
sessionMap.put(CONVERSATION_VIEW_MAP, conversationMap);
}
Map<String, Map> logicalMap = TypedCollections.dynamicallyCastMap(
(Map) conversationMap.get(conversationId),
String.class, Map.class);
if (logicalMap == null) {
if (conversationMap.size() == getNbOfConversationsInSession(ctx)) {
if (log.isDebugEnabled()) {
log.warn("Too many conversations, dumping the least recently used conversation ("
+ conversationMap.keySet().iterator().next()
+ ")");
}
}
logicalMap = new LRUMap<String, Map>(numberOfLogicalViews);
conversationMap.put(conversationId, logicalMap);
}
Object structure = stateToWrite[0];
Object savedState = handleSaveState(stateToWrite[1]);
String idInLogicalMap = (String) RequestStateManager.get(ctx,
RequestStateManager.LOGICAL_VIEW_MAP);
if (idInLogicalMap == null) {
idInLogicalMap = ((generateUniqueStateIds) ? createRandomId()
: createIncrementalRequestId(ctx));
}
String idInActualMap = null;
if (ctx.getPartialViewContext().isPartialRequest()) {
// If partial request, do not change actual view Id, because
// page not actually changed.
// Otherwise partial requests will soon overflow cache with
// values that would be never used.
idInActualMap = (String) RequestStateManager.get(ctx,
RequestStateManager.ACTUAL_VIEW_MAP);
}
if (null == idInActualMap) {
idInActualMap = ((generateUniqueStateIds) ? createRandomId()
: createIncrementalRequestId(ctx));
}
Map<String, Object[]> actualMap = TypedCollections.dynamicallyCastMap(
logicalMap.get(idInLogicalMap), String.class,
Object[].class);
if (actualMap == null) {
actualMap = new LRUMap<String, Object[]>(numberOfViews);
logicalMap.put(idInLogicalMap, actualMap);
}
id = idInLogicalMap + ':' + idInActualMap;
Object[] stateArray = actualMap.get(idInActualMap);
// reuse the array if possible
if (stateArray != null) {
stateArray[0] = structure;
stateArray[1] = savedState;
} else {
actualMap.put(idInActualMap, new Object[] { structure,
savedState });
}
conversationMap.put(conversationId, logicalMap);
// always call put/setAttribute as we may be in a clustered
// environment.
sessionMap.put(CONVERSATION_VIEW_MAP, conversationMap);
}
} else {
id = "stateless";
}
if (stateCapture != null) {
stateCapture.append(id);
} else {
ResponseWriter writer = ctx.getResponseWriter();
writer.startElement("input", null);
writer.writeAttribute("type", "hidden", null);
writer.writeAttribute("name",
ResponseStateManager.VIEW_STATE_PARAM, null);
if (webConfig.isOptionEnabled(EnableViewStateIdRendering)) {
String viewStateId = Util.getViewStateId(ctx);
writer.writeAttribute("id", viewStateId, null);
}
writer.writeAttribute("value", id, null);
if (webConfig.isOptionEnabled(AutoCompleteOffOnViewState)) {
writer.writeAttribute("autocomplete", "off", null);
}
writer.endElement("input");
writeClientWindowField(ctx, writer);
writeRenderKitIdField(ctx, writer);
}
}
@Override
public Object getState(FacesContext ctx, String viewId) {
String compoundId = getStateParamValue(ctx);
if (compoundId == null) {
return null;
}
if ("stateless".equals(compoundId)) {
return "stateless";
}
int sep = compoundId.indexOf(':');
assert (sep != -1);
assert (sep < compoundId.length());
String idInLogicalMap = compoundId.substring(0, sep);
String idInActualMap = compoundId.substring(sep + 1);
ExternalContext externalCtx = ctx.getExternalContext();
Object sessionObj = externalCtx.getSession(false);
// stop evaluating if the session is not available
if (sessionObj == null) {
if (log.isTraceEnabled()) {
log.trace(String.format(
"Unable to restore server side state for view ID %s as no session is available",
viewId));
}
return null;
}
// noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (sessionObj) {
Map<String, Map> conversationMap = (Map) externalCtx.getSessionMap().get(
CONVERSATION_VIEW_MAP);
if (conversationMap == null) {
return null;
}
Map logicalMap = null;
for (Map lm : conversationMap.values()) {
if (lm.get(idInLogicalMap) != null) {
logicalMap = lm;
break;
}
}
if (logicalMap != null) {
Map actualMap = (Map) logicalMap.get(idInLogicalMap);
if (actualMap != null) {
RequestStateManager.set(ctx,
RequestStateManager.LOGICAL_VIEW_MAP,
idInLogicalMap);
Object[] state = (Object[]) actualMap.get(idInActualMap);
Object[] restoredState = new Object[2];
restoredState[0] = state[0];
restoredState[1] = state[1];
if (state != null) {
RequestStateManager.set(ctx,
RequestStateManager.ACTUAL_VIEW_MAP,
idInActualMap);
if (state.length == 2 && state[1] != null) {
restoredState[1] = handleRestoreState(state[1]);
}
}
return restoredState;
}
}
}
return null;
}
/**
* @param ctx the <code>FacesContext</code> for the current request
* @return a unique ID for building the keys used to store views within a
* session
*/
private String createIncrementalRequestId(FacesContext ctx) {
Map<String, Object> sm = ctx.getExternalContext().getSessionMap();
AtomicInteger idgen = (AtomicInteger) sm.get(STATEMANAGED_SERIAL_ID_KEY);
if (idgen == null) {
idgen = new AtomicInteger(1);
}
// always call put/setAttribute as we may be in a clustered environment.
sm.put(STATEMANAGED_SERIAL_ID_KEY, idgen);
return (UIViewRoot.UNIQUE_ID_PREFIX + idgen.getAndIncrement());
}
private String createRandomId() {
return Long.valueOf(random.nextLong()).toString();
}
}