/*
* 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.context;
import static java.lang.String.format;
import static javax.servlet.RequestDispatcher.FORWARD_REQUEST_URI;
import static org.omnifaces.util.Faces.getContext;
import static org.omnifaces.util.Faces.responseReset;
import static org.omnifaces.util.Faces.setContextAttribute;
import static org.omnifaces.util.FacesLocal.getContextAttribute;
import static org.omnifaces.util.FacesLocal.getRequest;
import static org.omnifaces.util.FacesLocal.getRequestAttribute;
import static org.omnifaces.util.FacesLocal.getResponse;
import static org.omnifaces.util.FacesLocal.getViewId;
import static org.omnifaces.util.FacesLocal.invalidateSession;
import static org.omnifaces.util.FacesLocal.normalizeViewId;
import static org.omnifaces.util.Servlets.facesRedirect;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.faces.FacesException;
import javax.faces.application.ViewExpiredException;
import javax.faces.context.FacesContext;
import javax.faces.context.PartialResponseWriter;
import javax.faces.context.PartialViewContext;
import javax.faces.context.PartialViewContextWrapper;
import javax.faces.event.PhaseId;
import org.omnifaces.config.WebXml;
import org.omnifaces.exceptionhandler.FullAjaxExceptionHandler;
import org.omnifaces.util.Ajax;
import org.omnifaces.util.Hacks;
import org.omnifaces.util.Json;
/**
* <p>
* This OmniFaces partial view context extends and improves the standard partial view context as follows:
* <ul>
* <li>Support for executing callback scripts by {@link PartialResponseWriter#startEval()}.</li>
* <li>Support for adding arguments to an ajax response.</li>
* <li>Any XML tags which Mojarra and MyFaces has left open after an exception in rendering of an already committed
* ajax response, will now be properly closed. This prevents errors about malformed XML.</li>
* <li>Fixes the no-feedback problem when a {@link ViewExpiredException} occurs during an ajax request on a page which
* is restricted by <code>web.xml</code> <code><security-constraint></code>. The enduser will now properly be
* redirected to the login page instead of retrieving an ajax response with only a changed view state (and effectively
* thus no visual feedback at all).</li>
* </ul>
* You can use the {@link Ajax} utility class to easily add callback scripts and arguments.
* <p>
* This partial view context is already registered by OmniFaces' own <code>faces-config.xml</code> and thus gets
* auto-initialized when the OmniFaces JAR is bundled in a web application, so end-users do not need to register this
* partial view context explicitly themselves.
*
* @author Bauke Scholtz
* @since 1.2
* @see OmniPartialViewContextFactory
* @see FullAjaxExceptionHandler
* @see WebXml
* @see Ajax
* @see Json
*/
public class OmniPartialViewContext extends PartialViewContextWrapper {
// Constants ------------------------------------------------------------------------------------------------------
private static final String AJAX_DATA = "var OmniFaces=OmniFaces||{};OmniFaces.Ajax={data:%s};";
private static final String ERROR_NO_OMNI_PVC = "There is no current OmniPartialViewContext instance.";
// Variables ------------------------------------------------------------------------------------------------------
private PartialViewContext wrapped;
private Map<String, Object> arguments;
private List<String> callbackScripts;
private OmniPartialResponseWriter writer;
// Constructors ---------------------------------------------------------------------------------------------------
/**
* Construct a new OmniFaces partial view context around the given wrapped partial view context.
* @param wrapped The wrapped partial view context.
*/
public OmniPartialViewContext(PartialViewContext wrapped) {
this.wrapped = wrapped;
setCurrentInstance(this);
}
// Actions --------------------------------------------------------------------------------------------------------
@Override
public PartialResponseWriter getPartialResponseWriter() {
if (writer == null) {
writer = new OmniPartialResponseWriter(this, super.getPartialResponseWriter());
}
return writer;
}
/**
* An override which checks if the web.xml security constraint has been triggered during this ajax request
* (which can happen when the session has been timed out) and if so, then perform a redirect to the originally
* requested page. Otherwise the enduser ends up with an ajax response containing only the new view state
* without any form of visual feedback.
*/
@Override
public void processPartial(PhaseId phaseId) {
if (phaseId == PhaseId.RENDER_RESPONSE && redirectToFormLoginPageIfNecessary()) {
return;
}
super.processPartial(phaseId);
}
private boolean redirectToFormLoginPageIfNecessary() {
String loginURL = WebXml.INSTANCE.getFormLoginPage();
if (loginURL == null) {
return false;
}
FacesContext facesContext = FacesContext.getCurrentInstance();
String loginViewId = normalizeViewId(facesContext, loginURL);
if (!loginViewId.equals(getViewId(facesContext))) {
return false;
}
String originalURL = getRequestAttribute(facesContext, FORWARD_REQUEST_URI);
if (originalURL == null) {
return false;
}
try {
invalidateSession(facesContext); // Prevent server from remembering security constraint fail caused by ajax.
facesRedirect(getRequest(facesContext), getResponse(facesContext), originalURL); // This also clears cache.
return true;
}
catch (IOException e) {
throw new FacesException(e);
}
}
@Override // Necessary because this is missing in PartialViewContextWrapper (will be fixed in JSF 2.2).
public void setPartialRequest(boolean partialRequest) {
getWrapped().setPartialRequest(partialRequest);
}
@Override
public PartialViewContext getWrapped() {
return wrapped;
}
/**
* Add an argument to the partial response. This is as JSON object available by <code>OmniFaces.Ajax.data</code>.
* For supported argument value types, read {@link Json#encode(Object)}. If a given argument type is not supported,
* then an {@link IllegalArgumentException} will be thrown during end of render response.
* @param name The argument name.
* @param value The argument value.
*/
public void addArgument(String name, Object value) {
if (arguments == null) {
arguments = new HashMap<>();
}
arguments.put(name, value);
}
/**
* Add a callback script to the partial response. This script will be executed once the partial response is
* successfully retrieved at the client side.
* @param callbackScript The callback script to be added to the partial response.
*/
public void addCallbackScript(String callbackScript) {
if (callbackScripts == null) {
callbackScripts = new ArrayList<>();
}
callbackScripts.add(callbackScript);
}
/**
* Reset the partial response. This clears any JavaScript arguments and callbacks set any data written to the
* {@link PartialResponseWriter}.
* @see FullAjaxExceptionHandler
*/
public void resetPartialResponse() {
if (writer != null) {
writer.reset();
}
arguments = null;
callbackScripts = null;
}
/**
* Close the partial response. If the writer is still in update phase, then end the update and the document. This
* fixes the Mojarra problem of incomplete ajax responses caused by exceptions during ajax render response.
* @see FullAjaxExceptionHandler
*/
public void closePartialResponse() {
if (writer != null && writer.updating) {
try {
writer.endUpdate();
writer.endDocument();
}
catch (IOException e) {
throw new FacesException(e);
}
}
}
// Static ---------------------------------------------------------------------------------------------------------
/**
* Returns the current instance of the OmniFaces partial view context.
* @return The current instance of the OmniFaces partial view context.
* @throws IllegalStateException When there is no current instance of the OmniFaces partial view context. That can
* happen when the {@link OmniPartialViewContextFactory} is not properly registered, or when there's another
* {@link PartialViewContext} implementation which doesn't properly delegate through the wrapped instance.
*/
public static OmniPartialViewContext getCurrentInstance() {
return getCurrentInstance(getContext());
}
/**
* Returns the current instance of the OmniFaces partial view context from the given faces context.
* @param context The faces context to obtain the current instance of the OmniFaces partial view context from.
* @return The current instance of the OmniFaces partial view context from the given faces context.
* @throws IllegalStateException When there is no current instance of the OmniFaces partial view context. That can
* happen when the {@link OmniPartialViewContextFactory} is not properly registered, or when there's another
* {@link PartialViewContext} implementation which doesn't properly delegate through the wrapped instance.
*/
public static OmniPartialViewContext getCurrentInstance(FacesContext context) {
OmniPartialViewContext instance = getContextAttribute(context, OmniPartialViewContext.class.getName());
if (instance != null) {
return instance;
}
// Not found. Well, maybe the context attribute map was cleared for some reason. Get it once again.
instance = unwrap(context.getPartialViewContext());
if (instance != null) {
setCurrentInstance(instance);
return instance;
}
// Still not found. Well, maybe RichFaces is installed which doesn't use PartialViewContextWrapper.
if (Hacks.isRichFacesInstalled()) {
PartialViewContext pvc = Hacks.getRichFacesWrappedPartialViewContext();
if (pvc != null) {
instance = unwrap(pvc);
if (instance != null) {
setCurrentInstance(instance);
return instance;
}
}
}
// Still not found. Well, it's end of story.
throw new IllegalStateException(ERROR_NO_OMNI_PVC);
}
private static void setCurrentInstance(OmniPartialViewContext instance) {
setContextAttribute(OmniPartialViewContext.class.getName(), instance);
}
private static OmniPartialViewContext unwrap(PartialViewContext context) {
PartialViewContext unwrappedContext = context;
while (!(unwrappedContext instanceof OmniPartialViewContext) && unwrappedContext instanceof PartialViewContextWrapper) {
unwrappedContext = ((PartialViewContextWrapper) unwrappedContext).getWrapped();
}
if (unwrappedContext instanceof OmniPartialViewContext) {
return (OmniPartialViewContext) unwrappedContext;
}
else {
return null;
}
}
// Nested classes -------------------------------------------------------------------------------------------------
/**
* This OmniFaces partial response writer adds support for passing arguments to JavaScript context, executing
* oncomplete callback scripts, resetting the ajax response (specifically for {@link FullAjaxExceptionHandler}) and
* fixing incomlete XML response in case of exceptions.
* @author Bauke Scholtz
*/
private static class OmniPartialResponseWriter extends PartialResponseWriter {
// Variables --------------------------------------------------------------------------------------------------
private OmniPartialViewContext context;
private PartialResponseWriter wrapped;
private boolean updating;
// Constructors -----------------------------------------------------------------------------------------------
public OmniPartialResponseWriter(OmniPartialViewContext context, PartialResponseWriter wrapped) {
super(wrapped);
this.wrapped = wrapped; // We can't rely on getWrapped() due to MyFaces broken PartialResponseWriter.
this.context = context;
}
// Overridden actions -----------------------------------------------------------------------------------------
/**
* An override which remembers if we're updating or not.
* @see #endDocument()
* @see #reset()
*/
@Override
public void startUpdate(String targetId) throws IOException {
updating = true;
wrapped.startUpdate(targetId);
}
/**
* An override which remembers if we're updating or not.
* @see #endDocument()
* @see #reset()
*/
@Override
public void endUpdate() throws IOException {
updating = false;
wrapped.endUpdate();
}
/**
* An override which writes all {@link OmniPartialViewContext#arguments} as JSON to the extension and all
* {@link OmniPartialViewContext#callbackScripts} to the eval. It also checks if we're still updating, which
* may occur when MyFaces is used and an exception was thrown during rendering the partial response, and then
* gently closes the partial response which MyFaces has left open.
*/
@Override
public void endDocument() throws IOException {
if (updating) {
// If endDocument() method is entered with updating=true, then it means that MyFaces is used and that
// an exception was been thrown during ajax render response. The following calls will gently close the
// partial response which MyFaces has left open.
// Mojarra never enters endDocument() method with updating=true, this is handled in reset() method.
endCDATA();
endUpdate();
}
else {
if (context.arguments != null) {
startEval();
write(format(AJAX_DATA, Json.encode(context.arguments)));
endEval();
}
if (context.callbackScripts != null) {
for (String callbackScript : context.callbackScripts) {
startEval();
write(callbackScript);
endEval();
}
}
}
wrapped.endDocument();
}
// Custom actions ---------------------------------------------------------------------------------------------
/**
* Reset the partial response writer. It checks if we're still updating, which may occur when Mojarra is used
* and an exception was thrown during rendering the partial response, and then gently closes the partial
* response which Mojarra has left open. This would clear the internal state of the wrapped partial response
* writer and thus make it ready for reuse without risking malformed XML.
*/
public void reset() {
try {
wrapped.flush(); // Note: this doesn't actually flush to writer, but clears internal state.
if (updating) {
// If reset() method is entered with updating=true, then it means that Mojarra is used and that
// an exception was been thrown during ajax render response. The following calls will gently close
// the partial response which Mojarra has left open.
// MyFaces never enters reset() method with updating=true, this is handled in endDocument() method.
wrapped.startError("");
wrapped.endError();
wrapped.endElement("partial-response"); // Don't use endDocument() as it will flush.
}
}
catch (IOException e) {
throw new FacesException(e);
}
finally {
responseReset();
}
}
// Delegate actions -------------------------------------------------------------------------------------------
// Due to MyFaces broken PartialResponseWriter, which doesn't delegate to getWrapped() method, but instead to
// the local variable wrapped, we can't use getWrapped() in our own PartialResponseWriter implementations.
@Override
public void startError(String errorName) throws IOException {
wrapped.startError(errorName);
}
@Override
public void startEval() throws IOException {
wrapped.startEval();
}
@Override
public void startExtension(Map<String, String> attributes) throws IOException {
wrapped.startExtension(attributes);
}
@Override
public void startInsertAfter(String targetId) throws IOException {
wrapped.startInsertAfter(targetId);
}
@Override
public void startInsertBefore(String targetId) throws IOException {
wrapped.startInsertBefore(targetId);
}
@Override
public void endError() throws IOException {
wrapped.endError();
}
@Override
public void endEval() throws IOException {
wrapped.endEval();
}
@Override
public void endExtension() throws IOException {
wrapped.endExtension();
}
@Override
public void endInsert() throws IOException {
wrapped.endInsert();
}
@Override
public void delete(String targetId) throws IOException {
wrapped.delete(targetId);
}
@Override
public void redirect(String url) throws IOException {
wrapped.redirect(url);
}
@Override
public void updateAttributes(String targetId, Map<String, String> attributes) throws IOException {
wrapped.updateAttributes(targetId, attributes);
}
}
}