/*
* 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.taghandler;
import static java.lang.Boolean.TRUE;
import static java.lang.String.format;
import static org.omnifaces.util.Events.subscribeToRequestComponentEvent;
import static org.omnifaces.util.Facelets.getValueExpression;
import static org.omnifaces.util.Faces.getContext;
import static org.omnifaces.util.FacesLocal.redirect;
import static org.omnifaces.util.FacesLocal.responseSendError;
import static org.omnifaces.util.Messages.addFlashGlobalError;
import static org.omnifaces.util.Utils.coalesce;
import static org.omnifaces.util.Utils.isEmpty;
import java.io.IOException;
import java.util.Iterator;
import java.util.regex.Pattern;
import javax.el.ELContext;
import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewParameter;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.ComponentSystemEvent;
import javax.faces.event.PostValidateEvent;
import javax.faces.view.facelets.ComponentHandler;
import javax.faces.view.facelets.FaceletContext;
import javax.faces.view.facelets.TagConfig;
import javax.faces.view.facelets.TagHandler;
import org.omnifaces.util.Callback;
import org.omnifaces.util.Faces;
/**
* <p>
* <code><o:viewParamValidationFailed></code> allows the developer to handle a view parameter validation failure
* with either a redirect or an HTTP error status, optionally with respectively a flash message or HTTP error message.
* This tag can be placed inside <code><f:metadata></code> or <code><f|o:viewParam></code>. When placed in
* <code><f|o:viewParam></code>, it will be applied when the particular view parameter has a validation
* error as per {@link UIViewParameter#isValid()}. When placed in <code><f:metadata></code>, and no one view
* parameter has already handled the validation error via its own <code><o:viewParamValidationFailed></code>,
* it will be applied when there's a general validation error as per {@link FacesContext#isValidationFailed()}.
* <p>
* When the <code>sendRedirect</code> attribute is set, a call to {@link Faces#redirect(String, String...)} is made
* internally to send the redirect. So, the same rules as to scheme and leading slash apply here.
* When the <code>sendError</code> attribute is set, a call to {@link Faces#responseSendError(int, String)} is made
* internally to send the error. You can therefore customize HTTP error pages via <code><error-page></code>
* entries in <code>web.xml</code>. Otherwise the server-default one will be displayed instead.
*
* <h3><f:viewParam required="true"> fail</h3>
* <p>
* As a precaution; be aware that <code><f:viewParam required="true"></code> has a design error in current
* Mojarra and MyFaces releases (as of now, Mojarra 2.2.7 and MyFaces 2.2.4). When the parameter is not specified in
* the query string, it is retrieved as <code>null</code>, which causes an internal <code>isRequired()</code> check to
* be performed instead of delegating the check to the standard <code>UIInput</code> implementation. This has the
* consequence that <code>PreValidateEvent</code> and <code>PostValidateEvent</code> listeners are never invoked, which
* the <code><o:viewParamValidationFailed></code> is actually relying on. This is fixed in
* <code><o:viewParam></code>.
*
* <h3>Examples</h3>
* <p>
* In the example below the client will be presented an HTTP 400 error when at least one view param is absent.
* <pre>
* <f:metadata>
* <o:viewParam name="foo" required="true" />
* <o:viewParam name="bar" required="true" />
* <o:viewParamValidationFailed sendError="400" />
* </f:metadata>
* </pre>
* <p>
* In the example below the client will be redirected to "login.xhtml" when the "foo" parameter is absent, regardless of
* the "bar" parameter. When the "foo" parameter is present, but the "bar" parameter is absent, nothing new will happen.
* The process will proceed "as usual". I.e. the validation error will end up as a faces message in the current view the
* usual way.
* <pre>
* <f:metadata>
* <o:viewParam name="foo" required="true">
* <o:viewParamValidationFailed sendRedirect="login.xhtml" />
* </o:viewParam>
* <o:viewParam name="bar" required="true" />
* </f:metadata>
* </pre>
* <p>
* In the example below the client will be presented an HTTP 401 error when the "foo" parameter is absent, regardless of
* the "bar" or "baz" parameters. When the "foo" parameter is present, but either the "bar" or "baz" parameter is
* absent, the client will be redirected to "search.xhtml".
* <pre>
* <f:metadata>
* <o:viewParam name="foo" required="true">
* <o:viewParamValidationFailed sendError="401" />
* </o:viewParam>
* <o:viewParam name="bar" required="true" />
* <o:viewParam name="baz" required="true" />
* <o:viewParamValidationFailed sendRedirect="search.xhtml" />
* </f:metadata>
* </pre>
* <p>
* In a nutshell: when there are multiple <code><o:viewParamValidationFailed></code> tags, they will be
* applied in the same order as they are declared in the view. So, with the example above, the one nested in
* <code><f|o:viewParam></code> takes precedence over the one nested in <code><f:metadata></code>.
*
* <h3>Messaging</h3>
* <p>
* By default, the first occurring faces message on the parent component will be copied, or when there is none then the
* first occurring global faces message will be copied. When <code>sendRedirect</code> is used, it will be set
* as a global flash error message. When <code>sendError</code> is used, it will be set as HTTP status message.
* <p>
* You can override this message by explicitly specifying the <code>message</code> attribute. This is applicable for
* both <code>sendRedirect</code> and <code>sendError</code>.
* <pre>
* <o:viewParamValidationFailed sendRedirect="search.xhtml" message="You need to perform a search." />
* ...
* <o:viewParamValidationFailed sendError="401" message="Authentication failed. You need to login." />
* </pre>
*
* <p>
* Note, although all of above examples use <code>required="true"</code>, this does not mean that you can only use
* <code><o:viewParamValidationFailed></code> in combination with <code>required="true"</code> validation. You
* can use it in combination with any kind of conversion/validation on <code><f|o:viewParam></code>, even bean
* validation.
*
* <h3>Design notes</h3>
* <p>
* You can technically nest multiple <code><o:viewParamValidationFailed></code> inside the same parent, but this
* is not the documented approach and only the first one would be used.
* <p>
* You can <strong>not</strong> change the HTTP status code of a redirect. This is not a JSF limitation, but an HTTP
* limitation. The status code of a redirect will <strong>always</strong> end up as the one of the redirected response.
* If you intend to "redirect" with a different HTTP status code, then you should be using <code>sendError</code>
* instead and specify the desired page as <code><error-page></code> in <code>web.xml</code>.
*
* @author Bauke Scholtz
* @since 2.0
*/
public class ViewParamValidationFailed extends TagHandler {
// Constants ------------------------------------------------------------------------------------------------------
private static final Pattern HTTP_STATUS_CODE = Pattern.compile("[1-9][0-9][0-9]");
private static final String ERROR_INVALID_PARENT =
"%s This must be a child of UIViewRoot or UIViewParameter. Encountered parent of type '%s'."
+ " You need to enclose it in f:metadata or f|o:viewParam.";
private static final String ERROR_MISSING_ATTRIBUTE =
"%s You need to specify either 'sendRedirect' or 'sendError' attribute.";
private static final String ERROR_DOUBLE_ATTRIBUTE =
"%s You cannot specify both 'sendRedirect' and 'sendError' attributes. You can specify only one of them.";
private static final String ERROR_REQUIRED_ATTRIBUTE =
"%s This attribute is required, it cannot be set to null.";
private static final String ERROR_INVALID_SENDERROR =
"%s This attribute must represent a 3-digit HTTP status code. Encountered an invalid value '%s'.";
// Properties -----------------------------------------------------------------------------------------------------
private ValueExpression sendRedirect;
private ValueExpression sendError;
private ValueExpression message;
// Constructors ---------------------------------------------------------------------------------------------------
/**
* The tag constructor.
* @param config The tag config.
*/
public ViewParamValidationFailed(TagConfig config) {
super(config);
}
// Actions --------------------------------------------------------------------------------------------------------
/**
* If the parent component is an instance of {@link UIViewRoot} or {@link UIViewParameter} and is new, and the
* current request is <strong>not</strong> a postback, and <strong>not</strong> in render response, and all required
* attributes are set, then subscribe the parent component to the {@link PostValidateEvent}. This will invoke the
* {@link #processViewParamValidationFailed(ComponentSystemEvent)} method after validation.
* @throws IllegalStateException When the parent component is not an instance of {@link UIViewRoot} or
* {@link UIViewParameter}, or when there's already another <code><o:viewParamValidationFailed></code> tag
* registered on the same parent.
* @throws IllegalArgumentException When both <code>sendRedirect</code> and <code>sendError</code> attributes are
* missing or simultaneously specified.
*/
@Override
public void apply(FaceletContext context, final UIComponent parent) throws IOException {
if (!(parent instanceof UIViewRoot || parent instanceof UIViewParameter)) {
throw new IllegalStateException(format(ERROR_INVALID_PARENT, this, parent.getClass().getName()));
}
FacesContext facesContext = context.getFacesContext();
if (!ComponentHandler.isNew(parent) || facesContext.isPostback() || facesContext.getRenderResponse()) {
return;
}
sendRedirect = getValueExpression(context, getAttribute("sendRedirect"), String.class);
sendError = getValueExpression(context, getAttribute("sendError"), String.class);
message = getValueExpression(context, getAttribute("message"), String.class);
if (sendRedirect == null && sendError == null) {
throw new IllegalArgumentException(format(ERROR_MISSING_ATTRIBUTE, this));
}
else if (sendRedirect != null && sendError != null) {
throw new IllegalArgumentException(format(ERROR_DOUBLE_ATTRIBUTE, this));
}
subscribeToRequestComponentEvent(parent, PostValidateEvent.class, new Callback.WithArgument<ComponentSystemEvent>() {
@Override
public void invoke(ComponentSystemEvent event) {
processViewParamValidationFailed(event);
}
});
}
/**
* If the current request is <strong>not</strong> a postback and the current response is <strong>not</strong>
* already completed, and validation on the parent component has failed (for {@link UIViewRoot} this is checked by
* {@link FacesContext#isValidationFailed()} and for {@link UIViewParameter} this is checked by
* {@link UIViewParameter#isValid()}), then send either a redirect or error depending on the tag attributes set.
* @param event The component system event.
* @throws IllegalArgumentException When the <code>sendError</code> attribute does not represent a valid 3-digit
* HTTP status code.
*/
protected void processViewParamValidationFailed(ComponentSystemEvent event) {
FacesContext context = getContext();
UIComponent component = event.getComponent();
if (component instanceof UIViewParameter ? ((UIViewParameter) component).isValid() : !context.isValidationFailed()) {
return; // Validation has not failed.
}
if (TRUE.equals(context.getAttributes().put(getClass().getName(), TRUE))) {
return; // Validation fail has already been handled before. We can't send redirect or error multiple times.
}
String firstFacesMessage = coalesce(
cleanupFacesMessagesAndGetFirst(context.getMessages(component.getClientId(context))), // Prefer own message.
cleanupFacesMessagesAndGetFirst(context.getMessages(null)), // Then global messages.
cleanupFacesMessagesAndGetFirst(context.getMessages()) // Cleanup remainder.
);
evaluateAttributesAndHandleSendRedirectOrError(context, firstFacesMessage);
}
private String cleanupFacesMessagesAndGetFirst(Iterator<FacesMessage> facesMessages) {
String firstFacesMessage = null;
while (facesMessages.hasNext()) {
FacesMessage facesMessage = facesMessages.next();
if (firstFacesMessage == null) {
firstFacesMessage = facesMessage.getSummary();
}
facesMessages.remove(); // Avoid warning "Faces message has been enqueued but is not displayed".
}
return firstFacesMessage;
}
private void evaluateAttributesAndHandleSendRedirectOrError(FacesContext context, String defaultMessage) {
ELContext elContext = context.getELContext();
String evaluatedMessage = evaluate(elContext, message, false);
if (evaluatedMessage == null) {
evaluatedMessage = defaultMessage;
}
try {
if (sendRedirect != null) {
String evaluatedSendRedirect = evaluate(elContext, sendRedirect, true);
if (!isEmpty(evaluatedMessage)) {
addFlashGlobalError(evaluatedMessage);
}
redirect(context, evaluatedSendRedirect);
}
else {
String evaluatedSendError = evaluate(elContext, sendError, true);
if (!HTTP_STATUS_CODE.matcher(evaluatedSendError).matches()) {
throw new IllegalArgumentException(
format(ERROR_INVALID_SENDERROR, sendError, evaluatedSendError));
}
responseSendError(context, Integer.valueOf(evaluatedSendError), evaluatedMessage);
}
}
catch (IOException e) {
throw new FacesException(e);
}
}
// Helpers --------------------------------------------------------------------------------------------------------
/**
* Evaluate the given value expression as string.
*/
private static String evaluate(ELContext context, ValueExpression expression, boolean required) {
Object value = (expression != null) ? expression.getValue(context) : null;
if (required && isEmpty(value)) {
throw new IllegalArgumentException(format(ERROR_REQUIRED_ATTRIBUTE, expression));
}
return (value != null) ? value.toString() : null;
}
}