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}