/*
* Copyright 2017 OmniFaces
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.omnifaces.util;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static java.lang.String.format;
import static org.omnifaces.util.Components.getClosestParent;
import static org.omnifaces.util.FacesLocal.getApplicationAttribute;
import static org.omnifaces.util.FacesLocal.getContextAttribute;
import static org.omnifaces.util.FacesLocal.getInitParameter;
import static org.omnifaces.util.FacesLocal.getSessionAttribute;
import static org.omnifaces.util.FacesLocal.setContextAttribute;
import static org.omnifaces.util.Reflection.accessField;
import static org.omnifaces.util.Reflection.instance;
import static org.omnifaces.util.Reflection.invokeMethod;
import static org.omnifaces.util.Reflection.toClassOrNull;
import static org.omnifaces.util.Utils.coalesce;
import static org.omnifaces.util.Utils.unmodifiableSet;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.faces.component.StateHelper;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.context.FacesContextWrapper;
import javax.faces.context.PartialViewContext;
import javax.faces.context.PartialViewContextWrapper;
import javax.faces.render.ResponseStateManager;
import javax.websocket.Session;
import org.omnifaces.resourcehandler.ResourceIdentifier;
/**
* Collection of JSF implementation and/or JSF component library and/or server specific hacks.
*
* @author Bauke Scholtz
* @author Arjan Tijms
* @since 1.3
*/
public final class Hacks {
// Constants ------------------------------------------------------------------------------------------------------
private static final Set<String> RICHFACES_PVC_CLASS_NAMES =
unmodifiableSet(
"org.richfaces.context.ExtendedPartialViewContextImpl", // RichFaces 4.0-4.3.
"org.richfaces.context.ExtendedPartialViewContext"); // RichFaces 4.5+.
private static final boolean RICHFACES_INSTALLED = initRichFacesInstalled();
private static final String RICHFACES_RLR_RENDERER_TYPE =
"org.richfaces.renderkit.ResourceLibraryRenderer";
private static final String RICHFACES_RLF_CLASS_NAME =
"org.richfaces.resource.ResourceLibraryFactoryImpl";
private static final Class<UIComponent> PRIMEFACES_DIALOG_CLASS =
toClassOrNull("org.primefaces.component.dialog.Dialog");
private static final String MYFACES_PACKAGE_PREFIX = "org.apache.myfaces.";
private static final String MYFACES_RENDERED_SCRIPT_RESOURCES_KEY =
"org.apache.myfaces.RENDERED_SCRIPT_RESOURCES_SET";
private static final String MYFACES_RENDERED_STYLESHEET_RESOURCES_KEY =
"org.apache.myfaces.RENDERED_STYLESHEET_RESOURCES_SET";
private static final Set<String> MOJARRA_MYFACES_RESOURCE_DEPENDENCY_KEYS =
unmodifiableSet(
"com.sun.faces.PROCESSED_RESOURCE_DEPENDENCIES",
MYFACES_RENDERED_SCRIPT_RESOURCES_KEY,
MYFACES_RENDERED_STYLESHEET_RESOURCES_KEY);
private static final String MOJARRA_DEFAULT_RESOURCE_MAX_AGE = "com.sun.faces.defaultResourceMaxAge";
private static final String MYFACES_DEFAULT_RESOURCE_MAX_AGE = "org.apache.myfaces.RESOURCE_MAX_TIME_EXPIRES";
private static final long DEFAULT_RESOURCE_MAX_AGE = 604800000L; // 1 week.
private static final String[] PARAM_NAMES_RESOURCE_MAX_AGE = {
MOJARRA_DEFAULT_RESOURCE_MAX_AGE, MYFACES_DEFAULT_RESOURCE_MAX_AGE
};
private static final String MYFACES_RESOURCE_DEPENDENCY_UNIQUE_ID = "oam.view.resourceDependencyUniqueId";
private static final String MOJARRA_SERIALIZED_VIEWS = "com.sun.faces.renderkit.ServerSideStateHelper.LogicalViewMap";
private static final String MOJARRA_SERIALIZED_VIEW_KEY = "com.sun.faces.logicalViewMap";
private static final String MYFACES_SERIALIZED_VIEWS = "org.apache.myfaces.application.viewstate.ServerSideStateCacheImpl.SERIALIZED_VIEW";
private static final String MYFACES_VIEW_SCOPE_PROVIDER = "org.apache.myfaces.spi.ViewScopeProvider.INSTANCE";
private static final String ERROR_MAX_AGE =
"The '%s' init param must be a number. Encountered an invalid value of '%s'.";
// Lazy loaded properties (will only be initialized when FacesContext is available) -------------------------------
private static volatile Boolean myFacesUsed;
private static volatile Long defaultResourceMaxAge;
// Constructors/init ----------------------------------------------------------------------------------------------
private Hacks() {
//
}
private static boolean initRichFacesInstalled() {
for (String richFacesPvcClassName : RICHFACES_PVC_CLASS_NAMES) {
if (toClassOrNull(richFacesPvcClassName) != null) {
return true;
}
}
return false;
}
// RichFaces related ----------------------------------------------------------------------------------------------
/**
* Returns true if RichFaces 4.x is installed. That is, when the RichFaces 4.0-4.3 specific
* ExtendedPartialViewContextImpl, or RichFaces 4.5+ specific ExtendedPartialViewContext is present in the runtime
* classpath. As this is usually auto-registered, we may safely assume that RichFaces 4.x is installed.
* <p>
* Note that RichFaces 4.4 doesn't exist.
* @return Whether RichFaces 4.x is installed.
*/
public static boolean isRichFacesInstalled() {
return RICHFACES_INSTALLED;
}
/**
* RichFaces 4.0-4.3 ExtendedPartialViewContextImpl does not extend from PartialViewContextWrapper. So a hack wherin
* the exact fully qualified class name needs to be known has to be used to properly extract it from the
* {@link FacesContext#getPartialViewContext()}.
* @return The RichFaces PartialViewContext implementation.
*/
public static PartialViewContext getRichFacesPartialViewContext() {
PartialViewContext context = Ajax.getContext();
while (!RICHFACES_PVC_CLASS_NAMES.contains(context.getClass().getName())
&& context instanceof PartialViewContextWrapper)
{
context = ((PartialViewContextWrapper) context).getWrapped();
}
if (RICHFACES_PVC_CLASS_NAMES.contains(context.getClass().getName())) {
return context;
}
else {
return null;
}
}
/**
* RichFaces PartialViewContext implementation does not have the getRenderIds() method properly implemented. So a
* hack wherin the exact name of the private field needs to be known has to be used to properly extract it from the
* RichFaces PartialViewContext implementation.
* @return The render IDs from the RichFaces PartialViewContext implementation.
*/
public static Collection<String> getRichFacesRenderIds() {
PartialViewContext richFacesContext = getRichFacesPartialViewContext();
if (richFacesContext != null) {
Collection<String> renderIds = accessField(richFacesContext, "componentRenderIds");
if (renderIds != null) {
return renderIds;
}
}
return Collections.emptyList();
}
/**
* RichFaces 4.0-4.3 ExtendedPartialViewContextImpl does not have any getWrapped() method to return the wrapped
* PartialViewContext. So a reflection hack is necessary to return it from the private field.
* @return The wrapped PartialViewContext from the RichFaces 4.0-4.3 ExtendedPartialViewContextImpl.
*/
public static PartialViewContext getRichFacesWrappedPartialViewContext() {
PartialViewContext richFacesContext = getRichFacesPartialViewContext();
if (richFacesContext != null) {
return accessField(richFacesContext, "wrappedViewContext");
}
return null;
}
/**
* Returns true if the given renderer type is recognizeable as RichFaces resource library renderer.
* @param rendererType The renderer type to be checked.
* @return Whether the given renderer type is recognizeable as RichFaces resource library renderer.
*/
public static boolean isRichFacesResourceLibraryRenderer(String rendererType) {
return RICHFACES_RLR_RENDERER_TYPE.equals(rendererType);
}
/**
* Returns an ordered set of all JSF resource identifiers for the given RichFaces resource library resources.
* @param id The resource identifier of the RichFaces resource library (e.g. org.richfaces:ajax.reslib).
* @return An ordered set of all JSF resource identifiers for the given RichFaces resource library resources.
*/
@SuppressWarnings("rawtypes")
public static Set<ResourceIdentifier> getRichFacesResourceLibraryResources(ResourceIdentifier id) {
Object resourceFactory = instance(RICHFACES_RLF_CLASS_NAME);
String name = id.getName().split("\\.")[0];
Object resourceLibrary = invokeMethod(resourceFactory, "getResourceLibrary", name, id.getLibrary());
Iterable resources = invokeMethod(resourceLibrary, "getResources");
Set<ResourceIdentifier> resourceIdentifiers = new LinkedHashSet<>();
for (Object resource : resources) {
String libraryName = invokeMethod(resource, "getLibraryName");
String resourceName = invokeMethod(resource, "getResourceName");
resourceIdentifiers.add(new ResourceIdentifier(libraryName, resourceName));
}
return resourceIdentifiers;
}
// MyFaces related ------------------------------------------------------------------------------------------------
/**
* Returns true if MyFaces is used. That is, when the FacesContext instance is from the MyFaces specific package.
* @return Whether MyFaces is used.
* @since 1.8
*/
public static boolean isMyFacesUsed() {
if (myFacesUsed == null) {
FacesContext context = FacesContext.getCurrentInstance();
if (context != null) {
while (context instanceof FacesContextWrapper) {
context = ((FacesContextWrapper) context).getWrapped();
}
myFacesUsed = context.getClass().getPackage().getName().startsWith(MYFACES_PACKAGE_PREFIX);
}
else {
return false;
}
}
return myFacesUsed;
}
// JSF resource handling related ----------------------------------------------------------------------------------
/**
* Set the given script resource as rendered.
* @param context The involved faces context.
* @param id The resource identifier.
* @since 1.8
*/
public static void setScriptResourceRendered(FacesContext context, ResourceIdentifier id) {
setMojarraResourceRendered(context, id);
if (isMyFacesUsed()) {
setMyFacesResourceRendered(context, MYFACES_RENDERED_SCRIPT_RESOURCES_KEY, id);
}
}
/**
* Returns whether the given script resource is rendered.
* @param context The involved faces context.
* @param id The resource identifier.
* @return Whether the given script resource is rendered.
* @since 1.8
*/
public static boolean isScriptResourceRendered(FacesContext context, ResourceIdentifier id) {
boolean rendered = isMojarraResourceRendered(context, id);
if (!rendered && isMyFacesUsed()) {
return isMyFacesResourceRendered(context, MYFACES_RENDERED_SCRIPT_RESOURCES_KEY, id);
}
else {
return rendered;
}
}
/**
* Set the given stylesheet resource as rendered.
* @param context The involved faces context.
* @param id The resource identifier.
* @since 1.8
*/
public static void setStylesheetResourceRendered(FacesContext context, ResourceIdentifier id) {
setMojarraResourceRendered(context, id);
if (isMyFacesUsed()) {
setMyFacesResourceRendered(context, MYFACES_RENDERED_STYLESHEET_RESOURCES_KEY, id);
}
}
private static void setMojarraResourceRendered(FacesContext context, ResourceIdentifier id) {
context.getAttributes().put(id.getName() + id.getLibrary(), true);
}
private static boolean isMojarraResourceRendered(FacesContext context, ResourceIdentifier id) {
return context.getAttributes().containsKey(id.getName() + id.getLibrary());
}
private static void setMyFacesResourceRendered(FacesContext context, String key, ResourceIdentifier id) {
getMyFacesResourceMap(context, key).put(getMyFacesResourceKey(id), true);
}
private static boolean isMyFacesResourceRendered(FacesContext context, String key, ResourceIdentifier id) {
return getMyFacesResourceMap(context, key).containsKey(getMyFacesResourceKey(id));
}
private static Map<String, Boolean> getMyFacesResourceMap(FacesContext context, String key) {
Map<String, Boolean> map = getContextAttribute(context, key);
if (map == null) {
map = new HashMap<>();
setContextAttribute(context, key, map);
}
return map;
}
private static String getMyFacesResourceKey(ResourceIdentifier id) {
String library = id.getLibrary();
String name = id.getName();
return (library != null) ? (library + '/' + name) : name;
}
/**
* Returns the default resource maximum age in milliseconds.
* @return The default resource maximum age in milliseconds.
*/
public static long getDefaultResourceMaxAge() {
if (defaultResourceMaxAge == null) {
Long resourceMaxAge = DEFAULT_RESOURCE_MAX_AGE;
FacesContext context = FacesContext.getCurrentInstance();
if (context == null) {
return resourceMaxAge;
}
for (String name : PARAM_NAMES_RESOURCE_MAX_AGE) {
String value = getInitParameter(context, name);
if (value != null) {
try {
resourceMaxAge = Long.valueOf(value);
break;
}
catch (NumberFormatException e) {
throw new IllegalArgumentException(format(ERROR_MAX_AGE, name, value), e);
}
}
}
defaultResourceMaxAge = resourceMaxAge;
}
return defaultResourceMaxAge;
}
/**
* Remove the resource dependency processing related attributes from the given faces context.
* @param context The involved faces context.
*/
public static void removeResourceDependencyState(FacesContext context) {
// Mojarra and MyFaces remembers processed resource dependencies in a map.
context.getAttributes().keySet().removeAll(MOJARRA_MYFACES_RESOURCE_DEPENDENCY_KEYS);
// Mojarra and PrimeFaces puts "namelibrary=true" for every processed resource dependency.
// NOTE: This may possibly conflict with other keys with value=true. So far tested, this is harmless.
context.getAttributes().values().removeAll(Collections.singleton(true));
}
/**
* Set the unique ID of the component resource, taking into account MyFaces-specific way of generating a
* resource specific unique ID.
* @param context The involved faces context.
* @param resource The involved component resource.
* @since 2.6.1
*/
public static void setComponentResourceUniqueId(FacesContext context, UIComponent resource) {
UIViewRoot view = context.getViewRoot();
if (isMyFacesUsed()) {
view.getAttributes().put(MYFACES_RESOURCE_DEPENDENCY_UNIQUE_ID, TRUE);
}
try {
resource.setId(view.createUniqueId(context, null));
}
finally {
if (isMyFacesUsed()) {
view.getAttributes().put(MYFACES_RESOURCE_DEPENDENCY_UNIQUE_ID, FALSE);
}
}
}
// JSF state saving related --------------------------------------------------------------------------------------
/**
* Remove server side JSF view state associated with current request.
* @param context The involved faces context.
* @param manager The involved response state manager.
* @param viewId The view ID of the involved view.
* @since 2.3
*/
public static void removeViewState(FacesContext context, ResponseStateManager manager, String viewId) {
if (isMyFacesUsed()) {
Object state = invokeMethod(manager, "getSavedState", context);
if (!(state instanceof String)) {
return;
}
Object viewCollection = getSessionAttribute(context, MYFACES_SERIALIZED_VIEWS);
if (viewCollection == null) {
return;
}
Object stateCache = invokeMethod(manager, "getStateCache", context);
Integer stateId = invokeMethod(stateCache, "getServerStateId", context, state);
Serializable key = invokeMethod(invokeMethod(stateCache, "getSessionViewStorageFactory"), "createSerializedViewKey", context, viewId, stateId);
List<Serializable> keys = accessField(viewCollection, "_keys");
Map<Serializable, Object> serializedViews = accessField(viewCollection, "_serializedViews");
Map<Serializable, Serializable> precedence = accessField(viewCollection, "_precedence");
synchronized (viewCollection) { // Those fields are not concurrent maps.
keys.remove(key);
serializedViews.remove(key);
Serializable previousKey = precedence.remove(key);
if (previousKey != null) {
for (Entry<Serializable, Serializable> entry : precedence.entrySet()) {
if (entry.getValue().equals(key)) {
entry.setValue(previousKey);
}
}
}
Map<Serializable, String> viewScopeIds = accessField(viewCollection, "_viewScopeIds");
Map<String, Integer> viewScopeIdCounts = accessField(viewCollection, "_viewScopeIdCounts");
if (viewScopeIds == null || viewScopeIdCounts == null || viewScopeIds.get(key) == null) {
return; // Most likely cached page with client side state saving.
}
String viewScopeId = viewScopeIds.remove(key);
int count = coalesce(viewScopeIdCounts.get(viewScopeId), 1) - 1;
if (count < 1) {
viewScopeIdCounts.remove(viewScopeId);
invokeMethod(getApplicationAttribute(context, MYFACES_VIEW_SCOPE_PROVIDER), "destroyViewScopeMap", context, viewScopeId);
}
else {
viewScopeIdCounts.put(viewScopeId, count);
}
}
}
else { // Well, let's assume Mojarra.
Map<String, Object> views = getSessionAttribute(context, MOJARRA_SERIALIZED_VIEWS);
if (views != null) {
views.remove(context.getAttributes().get(MOJARRA_SERIALIZED_VIEW_KEY));
}
}
}
/**
* Expose protected state helper into public.
* @param component The component to obtain state helper for.
* @return The state helper of the given component.
* @since 2.3
*/
public static StateHelper getStateHelper(UIComponent component) {
return invokeMethod(component, "getStateHelper");
}
// PrimeFaces related ---------------------------------------------------------------------------------------------
/**
* Returns true if the current request is a PrimeFaces dynamic resource request.
* @param context The involved faces context.
* @return Whether the current request is a PrimeFaces dynamic resource request.
* @since 1.8
*/
public static boolean isPrimeFacesDynamicResourceRequest(FacesContext context) {
Map<String, String> params = context.getExternalContext().getRequestParameterMap();
return "primefaces".equals(params.get("ln")) && params.get("pfdrid") != null;
}
/**
* Returns true if the given components are nested in (same) PrimeFaces dialog.
* @param components The components to be checked.
* @return Whether the given components are nested in (same) PrimeFaces dialog.
* @since 2.6
*/
public static boolean isNestedInPrimeFacesDialog(UIComponent... components) {
if (PRIMEFACES_DIALOG_CLASS == null) {
return false;
}
Set<UIComponent> dialogs = new HashSet<>();
for (UIComponent component : components) {
dialogs.add(getClosestParent(component, PRIMEFACES_DIALOG_CLASS));
}
return dialogs.size() == 1 && dialogs.iterator().next() != null;
}
// Tomcat related -------------------------------------------------------------------------------------------------
/**
* Returns true if the given WS session is from Tomcat and given illegal state exception is caused by a push bomb
* which Tomcat couldn't handle. See also https://bz.apache.org/bugzilla/show_bug.cgi?id=56026 and
* https://github.com/omnifaces/omnifaces/issues/234
* @param session The WS session.
* @param illegalStateException The illegal state exception.
* @return Whether it was Tomcat who couldn't handle the push bomb.
* @since 2.5
*/
public static boolean isTomcatWebSocketBombed(Session session, IllegalStateException illegalStateException) {
return session.getClass().getName().startsWith("org.apache.tomcat.websocket.")
&& illegalStateException.getMessage().contains("[TEXT_FULL_WRITING]");
}
}