001/* 002 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 003 * 004 * Copyright (c) 1997-2013 Oracle and/or its affiliates. All rights reserved. 005 * 006 * The contents of this file are subject to the terms of either the GNU 007 * General Public License Version 2 only ("GPL") or the Common Development 008 * and Distribution License("CDDL") (collectively, the "License"). You 009 * may not use this file except in compliance with the License. You can 010 * obtain a copy of the License at 011 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html 012 * or packager/legal/LICENSE.txt. See the License for the specific 013 * language governing permissions and limitations under the License. 014 * 015 * When distributing the software, include this License Header Notice in each 016 * file and include the License file at packager/legal/LICENSE.txt. 017 * 018 * GPL Classpath Exception: 019 * Oracle designates this particular file as subject to the "Classpath" 020 * exception as provided by Oracle in the GPL Version 2 section of the License 021 * file that accompanied this code. 022 * 023 * Modifications: 024 * If applicable, add the following below the License Header, with the fields 025 * enclosed by brackets [] replaced by your own identifying information: 026 * "Portions Copyright [year] [name of copyright owner]" 027 * 028 * Contributor(s): 029 * If you wish your version of this file to be governed by only the CDDL or 030 * only the GPL Version 2, indicate your decision by adding "[Contributor] 031 * elects to include this software in this distribution under the [CDDL or GPL 032 * Version 2] license." If you don't indicate a single choice of license, a 033 * recipient has the option to distribute your version of this file under 034 * either the CDDL, the GPL Version 2 or to extend the choice of license to 035 * its licensees as provided above. However, if you add GPL Version 2 code 036 * and therefore, elected the GPL Version 2 license, then the option applies 037 * only if the new code is made subject to such option by the copyright 038 * holder. 039 */ 040 041package org.nuxeo.ecm.platform.ui.web.application; 042 043import java.util.Map; 044import java.util.concurrent.atomic.AtomicInteger; 045import java.io.IOException; 046 047import javax.faces.FacesException; 048import javax.faces.context.FacesContext; 049import javax.faces.context.ExternalContext; 050import javax.faces.context.ResponseWriter; 051import javax.faces.component.UIViewRoot; 052 053import com.sun.faces.util.TypedCollections; 054import com.sun.faces.util.LRUMap; 055import com.sun.faces.util.Util; 056import com.sun.faces.util.RequestStateManager; 057 058import static com.sun.faces.config.WebConfiguration.BooleanWebContextInitParameter.AutoCompleteOffOnViewState; 059import static com.sun.faces.config.WebConfiguration.BooleanWebContextInitParameter.EnableViewStateIdRendering; 060 061import javax.faces.render.ResponseStateManager; 062 063import org.apache.commons.logging.Log; 064import org.apache.commons.logging.LogFactory; 065import org.jboss.seam.contexts.Contexts; 066import org.jboss.seam.core.Conversation; 067 068import com.sun.faces.renderkit.ServerSideStateHelper; 069 070/** 071 * @since 6.0 072 */ 073public class NuxeoServerSideStateHelper extends ServerSideStateHelper { 074 075 private static final Log log = LogFactory.getLog(NuxeoServerSideStateHelper.class); 076 077 public static final int DEFAULT_NUMBER_OF_CONVERSATIONS_IN_SESSION = 4; 078 079 public static final String NUMBER_OF_CONVERSATIONS_IN_SESSION = "nuxeo.jsf.numberOfConversationsInSession"; 080 081 protected static final String NO_LONGRUNNING_CONVERSATION_ID = "NOLRC"; 082 083 protected static Integer numbersOfConversationsInSession = null; 084 085 /** 086 * The top level attribute name for storing the state structures within the session. 087 */ 088 public static final String CONVERSATION_VIEW_MAP = NuxeoServerSideStateHelper.class.getName() 089 + ".ConversationViewMap"; 090 091 protected static int getNbOfConversationsInSession(FacesContext context) { 092 if (numbersOfConversationsInSession == null) { 093 ExternalContext externalContext = context.getExternalContext(); 094 String value = externalContext.getInitParameter(NUMBER_OF_CONVERSATIONS_IN_SESSION); 095 if (null == value) { 096 numbersOfConversationsInSession = DEFAULT_NUMBER_OF_CONVERSATIONS_IN_SESSION; 097 } else { 098 try { 099 numbersOfConversationsInSession = Integer.parseInt(value); 100 101 } catch (NumberFormatException e) { 102 throw new FacesException("Context parameter " + NUMBER_OF_CONVERSATIONS_IN_SESSION 103 + " must have integer value"); 104 } 105 } 106 } 107 return numbersOfConversationsInSession; 108 } 109 110 @Override 111 public void writeState(FacesContext ctx, Object state, StringBuilder stateCapture) throws IOException { 112 113 Util.notNull("context", ctx); 114 115 String id; 116 117 if (!ctx.getViewRoot().isTransient()) { 118 Util.notNull("state", state); 119 Object[] stateToWrite = (Object[]) state; 120 ExternalContext externalContext = ctx.getExternalContext(); 121 Object sessionObj = externalContext.getSession(true); 122 Map<String, Object> sessionMap = externalContext.getSessionMap(); 123 124 // noinspection SynchronizationOnLocalVariableOrMethodParameter 125 synchronized (sessionObj) { 126 String conversationId = NO_LONGRUNNING_CONVERSATION_ID; 127 if (Contexts.isConversationContextActive() && Conversation.instance().isLongRunning()) { 128 conversationId = Conversation.instance().getId(); 129 } 130 131 Map<String, Map> conversationMap = TypedCollections.dynamicallyCastMap( 132 (Map) sessionMap.get(CONVERSATION_VIEW_MAP), String.class, Map.class); 133 if (conversationMap == null) { 134 conversationMap = new LRUMap<String, Map>(getNbOfConversationsInSession(ctx)); 135 sessionMap.put(CONVERSATION_VIEW_MAP, conversationMap); 136 } 137 138 Map<String, Map> logicalMap = TypedCollections.dynamicallyCastMap( 139 conversationMap.get(conversationId), String.class, Map.class); 140 if (logicalMap == null) { 141 if (conversationMap.size() == getNbOfConversationsInSession(ctx)) { 142 if (log.isDebugEnabled()) { 143 log.warn("Too many conversations, dumping the least recently used conversation (" 144 + conversationMap.keySet().iterator().next() + ")"); 145 } 146 } 147 logicalMap = new LRUMap<String, Map>(numberOfLogicalViews); 148 conversationMap.put(conversationId, logicalMap); 149 } 150 151 Object structure = stateToWrite[0]; 152 Object savedState = handleSaveState(stateToWrite[1]); 153 154 String idInLogicalMap = (String) RequestStateManager.get(ctx, RequestStateManager.LOGICAL_VIEW_MAP); 155 if (idInLogicalMap == null) { 156 idInLogicalMap = ((generateUniqueStateIds) ? createRandomId() : createIncrementalRequestId(ctx)); 157 } 158 String idInActualMap = null; 159 if (ctx.getPartialViewContext().isPartialRequest()) { 160 // If partial request, do not change actual view Id, because 161 // page not actually changed. 162 // Otherwise partial requests will soon overflow cache with 163 // values that would be never used. 164 idInActualMap = (String) RequestStateManager.get(ctx, RequestStateManager.ACTUAL_VIEW_MAP); 165 } 166 if (null == idInActualMap) { 167 idInActualMap = ((generateUniqueStateIds) ? createRandomId() : createIncrementalRequestId(ctx)); 168 } 169 Map<String, Object[]> actualMap = TypedCollections.dynamicallyCastMap(logicalMap.get(idInLogicalMap), 170 String.class, Object[].class); 171 if (actualMap == null) { 172 actualMap = new LRUMap<String, Object[]>(numberOfViews); 173 logicalMap.put(idInLogicalMap, actualMap); 174 } 175 176 id = idInLogicalMap + ':' + idInActualMap; 177 178 Object[] stateArray = actualMap.get(idInActualMap); 179 // reuse the array if possible 180 if (stateArray != null) { 181 stateArray[0] = structure; 182 stateArray[1] = savedState; 183 } else { 184 actualMap.put(idInActualMap, new Object[] { structure, savedState }); 185 } 186 187 conversationMap.put(conversationId, logicalMap); 188 // always call put/setAttribute as we may be in a clustered 189 // environment. 190 sessionMap.put(CONVERSATION_VIEW_MAP, conversationMap); 191 } 192 } else { 193 id = "stateless"; 194 } 195 196 if (stateCapture != null) { 197 stateCapture.append(id); 198 } else { 199 ResponseWriter writer = ctx.getResponseWriter(); 200 201 writer.startElement("input", null); 202 writer.writeAttribute("type", "hidden", null); 203 writer.writeAttribute("name", ResponseStateManager.VIEW_STATE_PARAM, null); 204 if (webConfig.isOptionEnabled(EnableViewStateIdRendering)) { 205 String viewStateId = Util.getViewStateId(ctx); 206 writer.writeAttribute("id", viewStateId, null); 207 } 208 writer.writeAttribute("value", id, null); 209 if (webConfig.isOptionEnabled(AutoCompleteOffOnViewState)) { 210 writer.writeAttribute("autocomplete", "off", null); 211 } 212 writer.endElement("input"); 213 214 writeClientWindowField(ctx, writer); 215 writeRenderKitIdField(ctx, writer); 216 } 217 } 218 219 @Override 220 public Object getState(FacesContext ctx, String viewId) { 221 222 String compoundId = getStateParamValue(ctx); 223 224 if (compoundId == null) { 225 return null; 226 } 227 228 if ("stateless".equals(compoundId)) { 229 return "stateless"; 230 } 231 232 int sep = compoundId.indexOf(':'); 233 assert (sep != -1); 234 assert (sep < compoundId.length()); 235 236 String idInLogicalMap = compoundId.substring(0, sep); 237 String idInActualMap = compoundId.substring(sep + 1); 238 239 ExternalContext externalCtx = ctx.getExternalContext(); 240 Object sessionObj = externalCtx.getSession(false); 241 242 // stop evaluating if the session is not available 243 if (sessionObj == null) { 244 if (log.isTraceEnabled()) { 245 log.trace(String.format( 246 "Unable to restore server side state for view ID %s as no session is available", viewId)); 247 } 248 return null; 249 } 250 251 // noinspection SynchronizationOnLocalVariableOrMethodParameter 252 synchronized (sessionObj) { 253 Map<String, Map> conversationMap = (Map) externalCtx.getSessionMap().get(CONVERSATION_VIEW_MAP); 254 if (conversationMap == null) { 255 return null; 256 } 257 Map logicalMap = null; 258 for (Map lm : conversationMap.values()) { 259 if (lm.get(idInLogicalMap) != null) { 260 logicalMap = lm; 261 break; 262 } 263 } 264 265 if (logicalMap != null) { 266 Map actualMap = (Map) logicalMap.get(idInLogicalMap); 267 if (actualMap != null) { 268 RequestStateManager.set(ctx, RequestStateManager.LOGICAL_VIEW_MAP, idInLogicalMap); 269 Object[] state = (Object[]) actualMap.get(idInActualMap); 270 Object[] restoredState = new Object[2]; 271 272 restoredState[0] = state[0]; 273 restoredState[1] = state[1]; 274 275 if (state != null) { 276 RequestStateManager.set(ctx, RequestStateManager.ACTUAL_VIEW_MAP, idInActualMap); 277 if (state.length == 2 && state[1] != null) { 278 restoredState[1] = handleRestoreState(state[1]); 279 } 280 } 281 282 return restoredState; 283 } 284 } 285 } 286 287 return null; 288 289 } 290 291 /** 292 * @param ctx the <code>FacesContext</code> for the current request 293 * @return a unique ID for building the keys used to store views within a session 294 */ 295 private String createIncrementalRequestId(FacesContext ctx) { 296 Map<String, Object> sm = ctx.getExternalContext().getSessionMap(); 297 AtomicInteger idgen = (AtomicInteger) sm.get(STATEMANAGED_SERIAL_ID_KEY); 298 if (idgen == null) { 299 idgen = new AtomicInteger(1); 300 } 301 // always call put/setAttribute as we may be in a clustered environment. 302 sm.put(STATEMANAGED_SERIAL_ID_KEY, idgen); 303 return (UIViewRoot.UNIQUE_ID_PREFIX + idgen.getAndIncrement()); 304 305 } 306 307 private String createRandomId() { 308 return Long.valueOf(random.nextLong()).toString(); 309 } 310 311}