All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.faces.context.flash.ELFlash Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2023 Contributors to Eclipse Foundation.
 * Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package com.sun.faces.context.flash;

import static com.sun.faces.config.WebConfiguration.BooleanWebContextInitParameter.EnableDistributable;
import static com.sun.faces.config.WebConfiguration.BooleanWebContextInitParameter.ForceAlwaysWriteFlashCookie;
import static java.nio.charset.StandardCharsets.UTF_8;

import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.sun.faces.config.WebConfiguration;
import com.sun.faces.config.WebConfiguration.WebContextInitParameter;
import com.sun.faces.facelets.tag.ui.UIDebug;
import com.sun.faces.util.ByteArrayGuardAESCTR;
import com.sun.faces.util.FacesLogger;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.UIViewRoot;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.Flash;
import jakarta.faces.event.PhaseId;
import jakarta.faces.event.PostKeepFlashValueEvent;
import jakarta.faces.event.PostPutFlashValueEvent;
import jakarta.faces.event.PreClearFlashEvent;
import jakarta.faces.event.PreRemoveFlashValueEvent;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;

/**
 * 

* How this implementation works *

* *

* This class is an application singleton. It has one ivar, innerMap. Entries are added to and removed from this map as * needed according to how the flash scope is defined in the spec. This implementation never touches the session, nor * does it cause the session to be created. *

* *

* Most of the hairy logic is encapsulated with in the inner class PreviousNextFlashInfoManager. An instance of this * class is obtained by calling one of the variants of getCurrentFlashManager(). When the instance is no longer needed * for this request, call releaseCurrentFlashManager(). *

* *

* Two very important methods are getPhaseMapForWriting() and getPhaseMapForReading(). These methods are the basis for * the Map implementation methods. Methods that need to write to the map use getPhaseMapForWriting(), those that need to * read use getPhaseMapForReading(). These methods allow for the laziness that allows us to only incur a cost when the * flash is actually written to. *

* *

* The operation of this class is intimately tied to the request processing lifecycle. Let's break down every run thru * the request processing lifecycle into two parts called "previous" and "next". We use the names "previous" and "next" * to indicate the persistence and timing of the data that is stored in the flash. Consider two runs through the requset * processing lifecle: N and N+1. On request N, there is no "previous" request. On Request N, any writes to the flash * that happen during RENDER RESPONSE go to the "next" flash map. This means they are available for the ENTIRE run * though the request processing lifecycle on request N+1. Any entries put into the "next" flash map on request N will * be expired at the end of request N+1. Now, when we get into request N+1 what was considered "next" on request N, is * now called "previous" from the perspective of request N+1. Any reads from the flash during request N+1 come from the * "previous" map. Any writes to the flash before RENDER RESPONSE go to the "previous" map. Any writes to the flash * during RENDER RESPNOSE go to the "next" map. *

*/ public class ELFlash extends Flash { private static final String ELEMENT_TYPE_MISMATCH = "element-type-mismatch"; private static final Logger LOGGER = FacesLogger.FLASH.getLogger(); /** *

* Keys in this map are the string version of sequence numbers obtained via calls to {@link #getNewSequenceNumber}. * Values are the actual Map instances that back the actual Map methods on this class. All writes to and reads from this * map are done by the {@link PreviousNextFlashInfoManager} inner class. *

* */ private Map> flashInnerMap = null; private final AtomicLong sequenceNumber = new AtomicLong(0); private int numberOfConcurentFlashUsers = Integer.parseInt(WebContextInitParameter.NumberOfConcurrentFlashUsers.getDefaultValue()); private long numberOfFlashesBetweenFlashReapings = Long.parseLong(WebContextInitParameter.NumberOfFlashesBetweenFlashReapings.getDefaultValue()); private final boolean distributable; private final ByteArrayGuardAESCTR guard; /** *

* These constants are referenced from other source files in this package. This one is a disambiguator prefix. *

*/ static final String PREFIX = "csfcf"; /** *

* This constant is used as the key in the application map that stores the singleton ELFlash instance. *

*/ static final String FLASH_ATTRIBUTE_NAME = PREFIX + "f"; /** *

* This constant is used as the name of the cookie sent to the client. The cookie is used to allow the flash scope to be * used to support POST REDIRECT GET navigation. *

*/ static final String FLASH_COOKIE_NAME = PREFIX + "c"; /** *

* This constant is used as the key the request map used, in the FlashELResolver, to convey the name of the property * being accessed via 'now'. *

*/ static final String FLASH_NOW_REQUEST_KEY = FLASH_ATTRIBUTE_NAME + "n"; private enum CONSTANTS { /** * The key in the FacesContext attributes map (hereafter referred to as contextMap) for the request scoped * {@link PreviousNextFlashInfoManager}. */ RequestFlashManager, /** * At the beginning of every phase, we save the value of the facesContext.getResponseComplete() into the contextMap * under this key. We check this value after the phase to see if this is the phase where the user called * responseComplete(). This is important to cover cases when the user does some funny lifecycle stuff. */ SavedResponseCompleteFlagValue, /** * This is used as the key in the flash itself to store the messages if they are being tracked. */ FacesMessageAttributeName, /** * This is used as the key in the flash itself to track whether or not messages are being saved across request/response * boundaries. */ KeepAllMessagesAttributeName, /** * This key is used in the contextMap to indicate that the next get should be treated as a keep. * */ KeepFlagAttributeName, /** * This key is used in the contextMap to prevent setting the cookie twice. */ DidWriteCookieAttributeName, /** * Force setMaxAge(0) */ ForceSetMaxAgeZero, } /** Creates a new instance of ELFlash */ private ELFlash(ExternalContext extContext) { flashInnerMap = new ConcurrentHashMap<>(); WebConfiguration config = WebConfiguration.getInstance(extContext); String value; try { value = config.getOptionValue(WebContextInitParameter.NumberOfConcurrentFlashUsers); numberOfConcurentFlashUsers = Integer.parseInt(value); } catch (NumberFormatException nfe) { if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.log(Level.WARNING, "Unable to set number of concurrent flash users. Defaulting to {0}", numberOfConcurentFlashUsers); } } try { value = config.getOptionValue(WebContextInitParameter.NumberOfFlashesBetweenFlashReapings); numberOfFlashesBetweenFlashReapings = Long.parseLong(value); } catch (NumberFormatException nfe) { if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.log(Level.WARNING, "Unable to set number flashes between flash repaings. Defaulting to {0}", numberOfFlashesBetweenFlashReapings); } } distributable = config.isOptionEnabled(EnableDistributable); guard = new ByteArrayGuardAESCTR(); } /** *

* Returns the flash Map for this application. This is a convenience method that calls * FacesContext.getCurrentInstance() and then calls the overloaded getFlash() that takes a * FacesContext with it. *

* * @return The flash Map for this session. */ public static Map getFlash() { FacesContext context = FacesContext.getCurrentInstance(); return getFlash(context.getExternalContext(), true); } /** * * @param extContext the ExternalContext for this request. * * @param create true to create a new instance for this request if necessary; false to return * null if there's no instance in the current session. * * @return The flash Map for this session. */ static ELFlash getFlash(ExternalContext extContext, boolean create) { Map appMap = extContext.getApplicationMap(); ELFlash flash = (ELFlash) appMap.get(FLASH_ATTRIBUTE_NAME); if (null == flash && create) { synchronized (extContext.getContext()) { if (null == (flash = (ELFlash) appMap.get(FLASH_ATTRIBUTE_NAME))) { flash = new ELFlash(extContext); appMap.put(FLASH_ATTRIBUTE_NAME, flash); } } } /* * If we are in a clustered environment and a session is active, store a helper to ensure our innerMap gets successfully * replicated. */ if (appMap.get(EnableDistributable.getQualifiedName()) != null) { synchronized (extContext.getContext()) { if (extContext.getSession(false) != null) { SessionHelper sessionHelper = SessionHelper.getInstance(extContext); if (sessionHelper == null) { sessionHelper = new SessionHelper(); } sessionHelper.update(extContext, flash); } } } return flash; } // // @Override public boolean isKeepMessages() { boolean result = false; Map phaseMap; if (null != (phaseMap = loggingGetPhaseMapForReading(false))) { Object value = phaseMap.get(CONSTANTS.KeepAllMessagesAttributeName.toString()); result = null != value ? (Boolean) value : false; } return result; } @Override public void setKeepMessages(boolean newValue) { loggingGetPhaseMapForWriting(false).put(CONSTANTS.KeepAllMessagesAttributeName.toString(), newValue); } @Override public boolean isRedirect() { boolean result = false; FacesContext context = FacesContext.getCurrentInstance(); Map contextMap = context.getAttributes(); PreviousNextFlashInfoManager flashManager; if (null != (flashManager = getCurrentFlashManager(contextMap, false))) { result = flashManager.getPreviousRequestFlashInfo().isIsRedirect(); } return result; } // PENDING(edburns): I'm going to make an entry to the errata. This // method can't be implemented because the decision of whether or // not to redirect is made by the navigationHandler. @Override public void setRedirect(boolean newValue) { } @Override public Object get(Object key) { Object result = null; FacesContext context = FacesContext.getCurrentInstance(); if (null != key) { if (key.equals("keepMessages")) { result = isKeepMessages(); } else if (key.equals("redirect")) { result = isRedirect(); } else { if (isKeepFlagSet(context)) { result = getPhaseMapForReading().get(key); keep(key.toString()); clearKeepFlag(context); return result; } } } if (null == result) { result = getPhaseMapForReading().get(key); } if (distributable && context.getExternalContext().getSession(false) != null) { SessionHelper sessionHelper = SessionHelper.getInstance(context.getExternalContext()); if (sessionHelper != null) { sessionHelper.update(context.getExternalContext(), this); } } if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, "get({0}) = {1}", new Object[] { key, result }); } return result; } @Override public Object put(String key, Object value) { Boolean b = null; Object result = null; boolean wasSpecialPut = false; if (null != key) { if (key.equals("keepMessages")) { setKeepMessages(b = Boolean.parseBoolean((String) value)); wasSpecialPut = true; } if (key.equals("redirect")) { setRedirect(b = Boolean.parseBoolean((String) value)); wasSpecialPut = true; } } FacesContext context = FacesContext.getCurrentInstance(); if (!wasSpecialPut) { result = null == b ? getPhaseMapForWriting().put(key, value) : b; if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, "put({0},{1})", new Object[] { key, value }); } context.getApplication().publishEvent(context, PostPutFlashValueEvent.class, key); } if (distributable && context.getExternalContext().getSession(false) != null) { SessionHelper sessionHelper = SessionHelper.getInstance(context.getExternalContext()); if (sessionHelper != null) { sessionHelper.update(context.getExternalContext(), this); } } return result; } @Override public Object remove(Object key) { Object result = null; FacesContext context = FacesContext.getCurrentInstance(); context.getApplication().publishEvent(context, PreRemoveFlashValueEvent.class, key); result = getPhaseMapForWriting().remove(key); return result; } @Override public boolean containsKey(Object key) { boolean result = false; result = getPhaseMapForReading().containsKey(key); return result; } @Override public boolean containsValue(Object value) { return getPhaseMapForReading().containsValue(value); } @Override public void putAll(Map t) { getPhaseMapForWriting().putAll(t); } @Override public Collection values() { return getPhaseMapForReading().values(); } @Override public int size() { return getPhaseMapForReading().size(); } @Override public void clear() { getPhaseMapForWriting().clear(); } @Override protected Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } @Override public Set> entrySet() { Set> readingMapEntrySet = getPhaseMapForReading().entrySet(), writingMapEntrySet = getPhaseMapForWriting().entrySet(), result = null; result = new HashSet<>(); result.addAll(readingMapEntrySet); result.addAll(writingMapEntrySet); return result; } @Override public boolean isEmpty() { boolean readingMapIsEmpty = getPhaseMapForReading().isEmpty(), writingMapIsEmpty = getPhaseMapForWriting().isEmpty(), result = false; result = readingMapIsEmpty && writingMapIsEmpty; return result; } @Override public Set keySet() { Set readingMapKeySet = getPhaseMapForReading().keySet(), writingMapKeySet = getPhaseMapForWriting().keySet(), result = null; result = new HashSet<>(); result.addAll(readingMapKeySet); result.addAll(writingMapKeySet); return result; } @Override public void keep(String key) { FacesContext context = FacesContext.getCurrentInstance(); Map requestMap = context.getExternalContext().getRequestMap(); Map contextMap = context.getAttributes(); PreviousNextFlashInfoManager flashManager; if (null != (flashManager = getCurrentFlashManager(contextMap, true))) { Object toKeep; if (null == (toKeep = requestMap.remove(key))) { FlashInfo flashInfo = null; if (null != (flashInfo = flashManager.getPreviousRequestFlashInfo())) { toKeep = flashInfo.getFlashMap().get(key); } } if (null != toKeep) { getPhaseMapForWriting().put(key, toKeep); context.getApplication().publishEvent(context, PostKeepFlashValueEvent.class, key); } } } @Override public void putNow(String key, Object value) { FacesContext context = FacesContext.getCurrentInstance(); Map contextMap = context.getAttributes(); PreviousNextFlashInfoManager flashManager; if (null != (flashManager = getCurrentFlashManager(contextMap, true))) { FlashInfo flashInfo = null; if (null != (flashInfo = flashManager.getPreviousRequestFlashInfo())) { flashInfo.getFlashMap().put(key, value); } } } @Override public void doPrePhaseActions(FacesContext context) { PhaseId currentPhase = context.getCurrentPhaseId(); Map contextMap = context.getAttributes(); contextMap.put(CONSTANTS.SavedResponseCompleteFlagValue, context.getResponseComplete()); Cookie cookie = null; if (currentPhase.equals(PhaseId.RESTORE_VIEW)) { if (null != (cookie = getCookie(context.getExternalContext()))) { getCurrentFlashManager(context, contextMap, cookie); } if (isKeepMessages()) { restoreAllMessages(context); } } else if (currentPhase.equals(PhaseId.RENDER_RESPONSE) && contextMap.containsKey(ForceAlwaysWriteFlashCookie) && (Boolean) contextMap.get(ForceAlwaysWriteFlashCookie)) { PreviousNextFlashInfoManager flashManager = getCurrentFlashManager(contextMap, true); cookie = flashManager.encode(); if (null != cookie) { setCookie(context, flashManager, cookie, true); } else { if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.log(Level.WARNING, "faces.externalcontext.flash.force.write.cookie.failed"); } } } } @Override public void doPostPhaseActions(FacesContext context) { if (context.getAttributes().containsKey(ACT_AS_DO_LAST_PHASE_ACTIONS)) { Boolean outgoingResponseIsRedirect = (Boolean) context.getAttributes().get(ACT_AS_DO_LAST_PHASE_ACTIONS); doLastPhaseActions(context, outgoingResponseIsRedirect); return; } PhaseId currentPhase = context.getCurrentPhaseId(); Map contextMap = context.getAttributes(); boolean responseCompleteJustSetTrue = responseCompleteWasJustSetTrue(context, contextMap), lastPhaseForThisRequest = responseCompleteJustSetTrue || currentPhase == PhaseId.RENDER_RESPONSE; if (lastPhaseForThisRequest) { doLastPhaseActions(context, false); } } public static final String ACT_AS_DO_LAST_PHASE_ACTIONS = ELFlash.class.getPackage().getName() + ".ACT_AS_DO_LAST_PHASE_ACTIONS"; /** *

* This is the most magic of methods. There are several scenarios in which this method can be called, but the first time * it is called for a request it takes action, while on subsequent times it returns without taking action. This is due * to the call to {@link #releaseCurrentFlashManager}. After this call, any calls to {@link #getCurrentFlashManager} * will return null. *

* *

* Scenario 1: normal request ending. This will be called after the RENDER_RESPONSE phase executes. * outgoingResponseIsRedirect will be false. *

* *

* Scenario 2: navigationHandler asks extContext for redirect. In this case, extContext calls this method directly, * outgoingResponseIsRedirect will be true. *

* *

* Scenario 3: extContext.flushBuffer(): As far as I can tell, this was only called in the JSP case, but it's good to * call it from there anyway, because we need to write our cookie before the response is committed. * outgoingResponseIsRedirect is false. *

* *

* Scenario 4: after rendering the response in JSP, but before the buffer is flushed. In the JSP case, I've found this * necessary because the call to extContext.flushBuffer() is too late, the response has already been committed by that * point. outgoingResponseIsRedirect is false. *

* * @param context the involved faces context * @param outgoingResponseIsRedirect whether outgoing response is redirect */ public void doLastPhaseActions(FacesContext context, boolean outgoingResponseIsRedirect) { Map contextMap = context.getAttributes(); PreviousNextFlashInfoManager flashManager = getCurrentFlashManager(contextMap, false); if (null == flashManager) { return; } if (isKeepMessages()) { saveAllMessages(context); } releaseCurrentFlashManager(contextMap); // What we do in this if-else statement has consequences for // PreviousNextFlashInfoManager.decode(). if (outgoingResponseIsRedirect) { FlashInfo previousRequestFlashInfo = flashManager.getPreviousRequestFlashInfo(); // Next two methods are VITALLY IMPORTANT! previousRequestFlashInfo.setIsRedirect(true); flashManager.expireNext_MovePreviousToNext(); } else { FlashInfo flashInfo = flashManager.getPreviousRequestFlashInfo(); if (null != flashInfo && flashInfo.getLifetimeMarker() == LifetimeMarker.SecondTimeThru) { flashManager.expirePrevious(); } } if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, "---------------------------------------"); } setCookie(context, flashManager, flashManager.encode(), false); } void setFlashInnerMap(Map> flashInnerMap) { this.flashInnerMap = flashInnerMap; } Map> getFlashInnerMap() { return flashInnerMap; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("[\n"); for (Map.Entry entry : entrySet()) { builder.append("{").append(entry.getKey()).append(", ").append(entry.getValue()).append("}\n"); } builder.append("]\n"); return builder.toString(); } private void maybeWriteCookie(FacesContext context, PreviousNextFlashInfoManager flashManager) { FlashInfo flashInfo = flashManager.getPreviousRequestFlashInfo(); if (null != flashInfo && flashInfo.getLifetimeMarker() == LifetimeMarker.SecondTimeThru) { PreviousNextFlashInfoManager copiedFlashManager = flashManager.copyWithoutInnerMap(); copiedFlashManager.expirePrevious(); setCookie(context, flashManager, copiedFlashManager.encode(), false); } } static void setKeepFlag(FacesContext context) { context.getAttributes().put(CONSTANTS.KeepFlagAttributeName, Boolean.TRUE); } void clearKeepFlag(FacesContext context) { context.getAttributes().remove(CONSTANTS.KeepFlagAttributeName); } boolean isKeepFlagSet(FacesContext context) { return Boolean.TRUE == context.getAttributes().get(CONSTANTS.KeepFlagAttributeName); } private long getNewSequenceNumber() { long result = sequenceNumber.incrementAndGet(); if (0 == result % numberOfFlashesBetweenFlashReapings) { reapFlashes(); } if (result == Long.MAX_VALUE) { result = 1; sequenceNumber.set(1); } return result; } private void reapFlashes() { if (flashInnerMap.size() < numberOfConcurentFlashUsers) { return; } Set keys = flashInnerMap.keySet(); long sequenceNumberToTest, currentSequenceNumber = sequenceNumber.get(); Map curFlash; for (String cur : keys) { sequenceNumberToTest = Long.parseLong(cur); if (numberOfConcurentFlashUsers < currentSequenceNumber - sequenceNumberToTest) { if (null != (curFlash = flashInnerMap.get(cur))) { curFlash.clear(); } flashInnerMap.remove(cur); } } if (distributable && FacesContext.getCurrentInstance().getExternalContext().getSession(false) != null) { ExternalContext extContext = FacesContext.getCurrentInstance().getExternalContext(); SessionHelper sessionHelper = SessionHelper.getInstance(extContext); if (null != sessionHelper) { sessionHelper.remove(extContext); sessionHelper = new SessionHelper(); sessionHelper.update(extContext, this); } } } private boolean responseCompleteWasJustSetTrue(FacesContext context, Map contextMap) { boolean result = false; // If it was false, but it's now true, return true result = Boolean.FALSE == contextMap.get(CONSTANTS.SavedResponseCompleteFlagValue) && context.getResponseComplete(); return result; } private static String getLogPrefix(FacesContext context) { StringBuilder result = new StringBuilder(); ExternalContext extContext = context.getExternalContext(); Object request = extContext.getRequest(); if (request instanceof HttpServletRequest) { result.append(((HttpServletRequest) request).getMethod()).append(" "); } UIViewRoot root = context.getViewRoot(); if (null != root) { String viewId = root.getViewId(); if (null != viewId) { result.append(viewId).append(" "); } } return result.toString(); } private Map loggingGetPhaseMapForWriting(boolean loggingEnabled) { FacesContext context = FacesContext.getCurrentInstance(); Map result = null; PhaseId currentPhase = context.getCurrentPhaseId(); Map contextMap = context.getAttributes(); PreviousNextFlashInfoManager flashManager; if (null != (flashManager = getCurrentFlashManager(contextMap, true))) { FlashInfo flashInfo; boolean isDebugLog = loggingEnabled && LOGGER.isLoggable(Level.FINEST); if (currentPhase.getOrdinal() < PhaseId.RENDER_RESPONSE.getOrdinal()) { flashInfo = flashManager.getPreviousRequestFlashInfo(); if (isDebugLog) { LOGGER.log(Level.FINEST, "{0}previous[{1}]", new Object[] { getLogPrefix(context), flashInfo.getSequenceNumber() }); } } else { flashInfo = flashManager.getNextRequestFlashInfo(this, true); if (isDebugLog) { LOGGER.log(Level.FINEST, "{0}next[{1}]", new Object[] { getLogPrefix(context), flashInfo.getSequenceNumber() }); } maybeWriteCookie(context, flashManager); } result = flashInfo.getFlashMap(); } return result; } /** *

* If the current phase is earlier than RENDER_RESPONSE, return the map for the "previous" request. Otherwise, return * the map for the "next" request. Note that we use getCurrentFlashManager(contextMap,true). This is because if this * method is being called, we know we actually need the map, so we have to ensure the underlying data structure is * present before trying to access it. *

*/ private Map getPhaseMapForWriting() { return loggingGetPhaseMapForWriting(true); } private Map loggingGetPhaseMapForReading(boolean loggingEnabled) { FacesContext context = FacesContext.getCurrentInstance(); Map result = Collections.emptyMap(); Map contextMap = context.getAttributes(); PreviousNextFlashInfoManager flashManager; if (null != (flashManager = getCurrentFlashManager(contextMap, false))) { FlashInfo flashInfo; if (null != (flashInfo = flashManager.getPreviousRequestFlashInfo())) { boolean isDebugLog = loggingEnabled && LOGGER.isLoggable(Level.FINEST); if (isDebugLog) { LOGGER.log(Level.FINEST, "{0}previous[{1}]", new Object[] { getLogPrefix(context), flashInfo.getSequenceNumber() }); } result = flashInfo.getFlashMap(); } } return result; } /** *

* Always return the map for the "previous" request. Note that we use getCurrentFlashManager(contextMap,false). This is * because if this method is being called, and there is pre-existing data in the flash from a previous write, then the * PreviousNextFlashInfoManager will already have been created. If there is not pre-existing data, we don't create the * PreviousNextFlashInfoManager, and therefore just return the empty map. *

*/ private Map getPhaseMapForReading() { return loggingGetPhaseMapForReading(true); } void saveAllMessages(FacesContext context) { // take no action on the GET that comes after a REDIRECT Map contextMap = context.getAttributes(); PreviousNextFlashInfoManager flashManager; if (null == (flashManager = getCurrentFlashManager(contextMap, true))) { return; } if (flashManager.getPreviousRequestFlashInfo().isIsRedirect()) { return; } Iterator messageClientIds = context.getClientIdsWithMessages(); List facesMessages; Map> allFacesMessages = null; Iterator messageIter; String curMessageId; // Save all the FacesMessages into a Map, which we store in the flash. // Step 1, go through the FacesMessage instances for each clientId // in the messageClientIds list. while (messageClientIds.hasNext()) { curMessageId = messageClientIds.next(); // Get the messages for this clientId messageIter = context.getMessages(curMessageId); facesMessages = new ArrayList<>(); while (messageIter.hasNext()) { facesMessages.add(messageIter.next()); } // Add the list to the map if (null == allFacesMessages) { allFacesMessages = new HashMap<>(); } allFacesMessages.put(curMessageId, facesMessages); } facesMessages = null; // Step 2, go through the FacesMessages that do not have a client // id associated with them. messageIter = context.getMessages(null); // Make sure to overwrite the previous facesMessages list facesMessages = new ArrayList<>(); while (messageIter.hasNext()) { facesMessages.add(messageIter.next()); } if (null != facesMessages) { // Add the list to the map if (null == allFacesMessages) { allFacesMessages = new HashMap<>(); } allFacesMessages.put(null, facesMessages); } getPhaseMapForWriting().put(CONSTANTS.FacesMessageAttributeName.toString(), allFacesMessages); } @SuppressWarnings(ELEMENT_TYPE_MISMATCH) void restoreAllMessages(FacesContext context) { Map> allFacesMessages; Map phaseMap = getPhaseMapForReading(); List facesMessages; if (null != (allFacesMessages = (Map>) phaseMap.get(CONSTANTS.FacesMessageAttributeName.toString()))) { for (Map.Entry> cur : allFacesMessages.entrySet()) { if (null != (facesMessages = allFacesMessages.get(cur.getKey()))) { for (FacesMessage curMessage : facesMessages) { context.addMessage(cur.getKey(), curMessage); } } } phaseMap.remove(CONSTANTS.FacesMessageAttributeName.toString()); } } /** *

* Return the cookie that came from the browser, if any. *

*/ private Cookie getCookie(ExternalContext extContext) { Cookie result = null; result = (Cookie) extContext.getRequestCookieMap().get(FLASH_COOKIE_NAME); return result; } /** *

* Set the cookie iff the response was not yet committed. If the response was committed, log a warning. *

*/ private void setCookie(FacesContext context, PreviousNextFlashInfoManager flashManager, Cookie toSet, boolean forceWrite) { Map contextMap = context.getAttributes(); ExternalContext extContext = context.getExternalContext(); if (contextMap.containsKey(CONSTANTS.DidWriteCookieAttributeName)) { return; } FlashInfo nextFlash = flashManager.getNextRequestFlashInfo(), prevFlash = flashManager.getPreviousRequestFlashInfo(); if (context.getAttributes().containsKey(CONSTANTS.ForceSetMaxAgeZero)) { removeCookie(extContext, toSet); return; } // Don't try to write the cookie unless there is data in the flash. if (forceWrite || null != nextFlash && !nextFlash.getFlashMap().isEmpty() || null != prevFlash && !prevFlash.getFlashMap().isEmpty()) { if (extContext.isResponseCommitted()) { if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.log(Level.WARNING, "faces.externalcontext.flash.response.already.committed"); } if (nextFlash != null) { flashManager.expireNext(); } if (prevFlash != null) { flashManager.expirePrevious(); } } else { Map properties = new HashMap<>(); Object val; if (null != (val = toSet.getDomain())) { properties.put("domain", val); } if (null != (val = toSet.getMaxAge())) { properties.put("maxAge", val); } if (extContext.isSecure()) { properties.put("secure", Boolean.TRUE); } else if (null != (val = toSet.getSecure())) { properties.put("secure", val); } if (null != (val = toSet.getPath())) { properties.put("path", val); } properties.put("httpOnly", Boolean.TRUE); extContext.addResponseCookie(toSet.getName(), toSet.getValue(), !properties.isEmpty() ? properties : null); properties = null; } contextMap.put(CONSTANTS.DidWriteCookieAttributeName, Boolean.TRUE); } else { removeCookie(extContext, toSet); } } private void removeCookie(ExternalContext extContext, Cookie toRemove) { if (extContext.isResponseCommitted()) { return; } Map properties = new HashMap<>(); Object val; toRemove.setMaxAge(0); if (null != (val = toRemove.getDomain())) { properties.put("domain", val); } if (null != (val = toRemove.getMaxAge())) { properties.put("maxAge", val); } if (extContext.isSecure()) { properties.put("secure", Boolean.TRUE); } else if (null != (val = toRemove.getSecure())) { properties.put("secure", val); } if (null != (val = toRemove.getPath())) { properties.put("path", val); } properties.put("httpOnly", Boolean.TRUE); extContext.addResponseCookie(toRemove.getName(), toRemove.getValue(), !properties.isEmpty() ? properties : null); properties = null; } private enum LifetimeMarker { // these must be unique FirstTimeThru("f"), SecondTimeThru("s"), IsRedirect("r"), IsNormal("n"); private static char FIRST_TIME_THRU = 'f'; private static char SECOND_TIME_THRU = 's'; private static char IS_REDIRECT = 'r'; private static char IS_NORMAL = 'n'; private String name; private LifetimeMarker(String name) { this.name = name; } @Override public String toString() { return name; } public char encode() { return name.charAt(0); } public static LifetimeMarker decode(char c) { LifetimeMarker result = FirstTimeThru; if (FIRST_TIME_THRU == c) { result = FirstTimeThru; } else if (SECOND_TIME_THRU == c) { result = SecondTimeThru; } else if (IS_REDIRECT == c) { result = IsRedirect; } else if (IS_NORMAL == c) { result = IsNormal; } else { throw new IllegalStateException("class invariant failed: invalid lifetime marker"); } return result; } } private void releaseCurrentFlashManager(Map contextMap) { contextMap.remove(CONSTANTS.RequestFlashManager); } /** *

* Called when you need to get access to the flashManager. If argument create is true, and no instance of the * FlashManager exists for this request, create it and store it in the contextMap. *

*/ private PreviousNextFlashInfoManager getCurrentFlashManager(Map contextMap, boolean create) { PreviousNextFlashInfoManager result = (PreviousNextFlashInfoManager) contextMap.get(CONSTANTS.RequestFlashManager); if (null == result && create) { result = new PreviousNextFlashInfoManager(guard, flashInnerMap); result.initializeBaseCase(this); contextMap.put(CONSTANTS.RequestFlashManager, result); } return result; } /** *

* Called on the preRestoreView phase if the browser sent us a cookie. If no instance of the FlashManager exists for * this request, create it and store it in the contextMap. *

*/ private PreviousNextFlashInfoManager getCurrentFlashManager(FacesContext context, Map contextMap, Cookie cookie) { PreviousNextFlashInfoManager result = (PreviousNextFlashInfoManager) contextMap.get(CONSTANTS.RequestFlashManager); if (null == result) { result = new PreviousNextFlashInfoManager(guard, flashInnerMap); try { result.decode(context, this, cookie); contextMap.put(CONSTANTS.RequestFlashManager, result); } catch (InvalidKeyException ike) { contextMap.put(CONSTANTS.ForceSetMaxAgeZero, Boolean.TRUE); if (LOGGER.isLoggable(Level.SEVERE)) { result = getCurrentFlashManager(contextMap, true); LOGGER.log(Level.SEVERE, "faces.externalcontext.flash.bad.cookie", new Object[] { ike.getMessage() }); } } } return result; } /** *

* On any given request, there are actually two maps behind the flash. Which one is actually used on a given Map method * depends on the current lifecycle phase at the time the method is invoked. There is a "next" map, and a "previous" * map. This class manages the complexities of dealing with these two maps, and does so by relying on another inner * class, FlashInfo. *

* *

* The "next" map is used in only one case, which happens to be a VERY common case: write operations to the flash that * happen during render response. *

* *

* The "previous" map is used for write operations that happen before render response, and for all read operations. *

* *

* This class knows how to "decode" its state from an incoming cookie, written by a previous call to "encode". *

* *

* See the docs for FlashInfo for more information. *

*/ private static final class PreviousNextFlashInfoManager { private FlashInfo previousRequestFlashInfo; private FlashInfo nextRequestFlashInfo; private boolean incomingCookieCameFromRedirect = false; private Map> innerMap; private ByteArrayGuardAESCTR guard; private PreviousNextFlashInfoManager(ByteArrayGuardAESCTR guard) { this.guard = guard; } private PreviousNextFlashInfoManager(ByteArrayGuardAESCTR guard, Map> innerMap) { this.guard = guard; this.innerMap = innerMap; } protected PreviousNextFlashInfoManager copyWithoutInnerMap() { PreviousNextFlashInfoManager result = new PreviousNextFlashInfoManager(guard); result.innerMap = Collections.emptyMap(); if (null != previousRequestFlashInfo) { result.previousRequestFlashInfo = previousRequestFlashInfo.copyWithoutInnerMap(); } if (null != nextRequestFlashInfo) { result.nextRequestFlashInfo = nextRequestFlashInfo.copyWithoutInnerMap(); } result.incomingCookieCameFromRedirect = incomingCookieCameFromRedirect; return result; } @Override public String toString() { String result = null; result = "previousRequestSequenceNumber: " + (null != previousRequestFlashInfo ? previousRequestFlashInfo.getSequenceNumber() : "null") + " nextRequestSequenceNumber: " + (null != nextRequestFlashInfo ? nextRequestFlashInfo.getSequenceNumber() : "null"); return result; } void initializeBaseCase(ELFlash flash) { Map flashMap = null; previousRequestFlashInfo = new FlashInfo(flash.getNewSequenceNumber(), LifetimeMarker.FirstTimeThru, false); innerMap.put(previousRequestFlashInfo.getSequenceNumber() + "", flashMap = new HashMap<>()); previousRequestFlashInfo.setFlashMap(flashMap); nextRequestFlashInfo = new FlashInfo(flash.getNewSequenceNumber(), LifetimeMarker.FirstTimeThru, false); innerMap.put(nextRequestFlashInfo.getSequenceNumber() + "", flashMap = new HashMap<>()); nextRequestFlashInfo.setFlashMap(flashMap); } void expirePrevious() { // expire previous if (null != previousRequestFlashInfo) { Map flashMap; // clear the old map if (null != (flashMap = previousRequestFlashInfo.getFlashMap())) { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, "{0} expire previous[{1}]", new Object[] { getLogPrefix(FacesContext.getCurrentInstance()), previousRequestFlashInfo.getSequenceNumber() }); } FacesContext context = FacesContext.getCurrentInstance(); context.getApplication().publishEvent(context, PreClearFlashEvent.class, flashMap); flashMap.clear(); } // remove it from the flash innerMap.remove(previousRequestFlashInfo.getSequenceNumber() + ""); previousRequestFlashInfo = null; } } void expireNext() { // expire next if (null != nextRequestFlashInfo) { Map flashMap; // clear the old map if (null != (flashMap = nextRequestFlashInfo.getFlashMap())) { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, "{0} expire next[{1}]", new Object[] { getLogPrefix(FacesContext.getCurrentInstance()), nextRequestFlashInfo.getSequenceNumber() }); } FacesContext context = FacesContext.getCurrentInstance(); context.getApplication().publishEvent(context, PreClearFlashEvent.class, flashMap); flashMap.clear(); } // remove it from the flash innerMap.remove(nextRequestFlashInfo.getSequenceNumber() + ""); nextRequestFlashInfo = null; } } void expireNext_MovePreviousToNext() { if (null != nextRequestFlashInfo) { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, "{0} expire next[{1}], move previous to next", new Object[] { getLogPrefix(FacesContext.getCurrentInstance()), nextRequestFlashInfo.getSequenceNumber() }); } Map flashMap = nextRequestFlashInfo.getFlashMap(); FacesContext context = FacesContext.getCurrentInstance(); context.getApplication().publishEvent(context, PreClearFlashEvent.class, flashMap); // clear the old map flashMap.clear(); // remove it from the flash innerMap.remove(nextRequestFlashInfo.getSequenceNumber() + ""); nextRequestFlashInfo = null; } nextRequestFlashInfo = previousRequestFlashInfo; previousRequestFlashInfo = null; } /** *

* Decode the state of the PreviousNextFlashInfoManager from a Cookie. This entire method is wrapped in a try-catch * block to prevent any errors from malformed cookies from polluting the system. When any error occurs, the flash is not * usable for this request, and a nice error message is logged. *

* *

* This method is where the LifetimeMarker is incremented, UNLESS the incoming request is the GET after the REDIRECT * after POST, in which case we don't increment it because the system will expire the entries in the doLastPhaseActions. *

* */ void decode(FacesContext context, ELFlash flash, Cookie cookie) throws InvalidKeyException { String value = null; try { String urlDecodedValue = URLDecoder.decode(cookie.getValue(), UTF_8); value = guard.decrypt(urlDecodedValue); int i = value.indexOf("_"); // IMPORTANT: what was "next" when the cookie was // encoded is now "previous". Therefore decode "next" first. String temp = value.substring(0, i++); if (0 < temp.length()) { nextRequestFlashInfo = new FlashInfo(); nextRequestFlashInfo.decode(temp); } // invariant we must always have something after the _ previousRequestFlashInfo = new FlashInfo(); previousRequestFlashInfo.decode(value.substring(i)); // handle the consequences of action taken on doLastPhaseActions if (previousRequestFlashInfo.isIsRedirect()) { setIncomingCookieCameFromRedirect(true); previousRequestFlashInfo.setIsRedirect(false); } else { // Don't make the flash older on debug requests if (!UIDebug.debugRequest(context)) { previousRequestFlashInfo.setLifetimeMarker(LifetimeMarker.SecondTimeThru); expireNext(); } } Map flashMap; // If the browser sent a cookie that is valid, but // doesn't correspond to a map in memory... if (null == (flashMap = innerMap.get(previousRequestFlashInfo.getSequenceNumber() + ""))) { // create a new map previousRequestFlashInfo = new FlashInfo(); previousRequestFlashInfo.setSequenceNumber(flash.getNewSequenceNumber()); previousRequestFlashInfo.setLifetimeMarker(LifetimeMarker.FirstTimeThru); previousRequestFlashInfo.setIsRedirect(false); // put it in the flash innerMap.put(previousRequestFlashInfo.getSequenceNumber() + "", flashMap = new HashMap<>()); } previousRequestFlashInfo.setFlashMap(flashMap); if (null != nextRequestFlashInfo) { if (null == (flashMap = innerMap.get(nextRequestFlashInfo.getSequenceNumber() + ""))) { // create a new map nextRequestFlashInfo = new FlashInfo(); nextRequestFlashInfo.setSequenceNumber(flash.getNewSequenceNumber()); nextRequestFlashInfo.setLifetimeMarker(LifetimeMarker.FirstTimeThru); nextRequestFlashInfo.setIsRedirect(false); // put it in the flash innerMap.put(nextRequestFlashInfo.getSequenceNumber() + "", flashMap = new HashMap<>()); } nextRequestFlashInfo.setFlashMap(flashMap); } } catch (Throwable t) { context.getAttributes().put(CONSTANTS.ForceSetMaxAgeZero, Boolean.TRUE); if (LOGGER.isLoggable(Level.SEVERE)) { LOGGER.log(Level.SEVERE, "faces.externalcontext.flash.bad.cookie", new Object[] { value }); } } } /** *

* Encode the current state of the PreviousNextFlashInfoManager to the cookie. *

*/ Cookie encode() { String value = (null != previousRequestFlashInfo ? previousRequestFlashInfo.encode() : "") + "_" + (null != nextRequestFlashInfo ? nextRequestFlashInfo.encode() : ""); String encryptedValue = guard.encrypt(value); Cookie result = new Cookie(FLASH_COOKIE_NAME, URLEncoder.encode(encryptedValue, UTF_8)); if (1 == value.length()) { result.setMaxAge(0); } String requestContextPath = FacesContext.getCurrentInstance().getExternalContext().getRequestContextPath(); if (requestContextPath.isEmpty()) { requestContextPath = "/"; } result.setPath(requestContextPath); return result; } FlashInfo getPreviousRequestFlashInfo() { return previousRequestFlashInfo; } void setPreviousRequestFlashInfo(FlashInfo thisRequestFlashInfo) { previousRequestFlashInfo = thisRequestFlashInfo; } FlashInfo getNextRequestFlashInfo() { return nextRequestFlashInfo; } FlashInfo getNextRequestFlashInfo(ELFlash flash, boolean create) { if (create && null == nextRequestFlashInfo) { nextRequestFlashInfo = new FlashInfo(); nextRequestFlashInfo.setSequenceNumber(flash.getNewSequenceNumber()); nextRequestFlashInfo.setLifetimeMarker(LifetimeMarker.FirstTimeThru); nextRequestFlashInfo.setIsRedirect(false); // put it in the flash Map flashMap = null; innerMap.put(nextRequestFlashInfo.getSequenceNumber() + "", flashMap = new HashMap<>()); nextRequestFlashInfo.setFlashMap(flashMap); } return nextRequestFlashInfo; } void setNextRequestFlashInfo(FlashInfo nextRequestFlashInfo) { this.nextRequestFlashInfo = nextRequestFlashInfo; } boolean isIncomingCookieCameFromRedirect() { return incomingCookieCameFromRedirect; } void setIncomingCookieCameFromRedirect(boolean incomingCookieCameFromRedirect) { this.incomingCookieCameFromRedirect = incomingCookieCameFromRedirect; } } /** *

* Encapsulate one of the two maps that back the flash for the current request. *

*/ private static final class FlashInfo { /** *

* Set to true by the Flash when the extContext tells us there is a redirect. *

*/ private boolean isRedirect; /** *

* How many times has this map been through the lifecycle? *

*/ private LifetimeMarker lifetimeMarker; /** *

* Application Unique key in the innerMap. *

*/ private long sequenceNumber; /** *

* The Map that stores the data. This map itself is stored in innerMap under the key given by the value of * sequenceNumber. *

*/ private Map flashMap; private FlashInfo() { } FlashInfo(long sequenceNumber, LifetimeMarker lifetimeMarker, boolean isRedirect) { setSequenceNumber(sequenceNumber); setLifetimeMarker(lifetimeMarker); setIsRedirect(isRedirect); } FlashInfo copyWithoutInnerMap() { FlashInfo result = new FlashInfo(sequenceNumber, lifetimeMarker, isRedirect); return result; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final FlashInfo other = (FlashInfo) obj; if (isRedirect != other.isRedirect) { return false; } if (lifetimeMarker != other.lifetimeMarker && (lifetimeMarker == null || !lifetimeMarker.equals(other.lifetimeMarker))) { return false; } if (sequenceNumber != other.sequenceNumber) { return false; } return true; } @Override public int hashCode() { int hash = 7; hash = 71 * hash + (isRedirect ? 1 : 0); hash = 71 * hash + (lifetimeMarker != null ? lifetimeMarker.hashCode() : 0); hash = 71 * hash + (int) (sequenceNumber ^ sequenceNumber >>> 32); return hash; } void decode(String value) { if (null == value || 0 == value.length()) { // PENDING(edburns): REMOVE THIS return; } int i = value.indexOf('X'); // decode the sequence number setSequenceNumber(Long.parseLong(value.substring(0, i++))); // decode the lifetime marker setLifetimeMarker(LifetimeMarker.decode(value.charAt(i++))); // decode the redirect flag setIsRedirect(LifetimeMarker.IsRedirect == LifetimeMarker.decode(value.charAt(i++))); } String encode() { String value = null; // The cookie value is an encoding of the sequence number, the // lifetime marker, and the redirect flag if (isIsRedirect()) { value = Long.toString(getSequenceNumber()) + "X" + getLifetimeMarker().encode() + LifetimeMarker.IsRedirect.encode(); } else { value = Long.toString(getSequenceNumber()) + "X" + getLifetimeMarker().encode() + LifetimeMarker.IsNormal.encode(); } return value; } boolean isIsRedirect() { return isRedirect; } void setIsRedirect(boolean isRedirect) { this.isRedirect = isRedirect; } long getSequenceNumber() { return sequenceNumber; } void setSequenceNumber(long sequenceNumber) { this.sequenceNumber = sequenceNumber; } LifetimeMarker getLifetimeMarker() { return lifetimeMarker; } void setLifetimeMarker(LifetimeMarker lifetimeMarker) { this.lifetimeMarker = lifetimeMarker; } Map getFlashMap() { return flashMap; } void setFlashMap(Map flashMap) { this.flashMap = flashMap; } } }