Permalink
Browse files

Add <o:viewParamValidationFailed>

  • Loading branch information...
1 parent f8f84aa commit ca1e625447de7b5e33cb7e7d4085ad1b99b5844a @BalusC BalusC committed Jul 16, 2014
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2014 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 org.omnifaces.util.Events.addCallbackAfterPhaseListener;
+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.AbortProcessingException;
+import javax.faces.event.ComponentSystemEvent;
+import javax.faces.event.ComponentSystemEventListener;
+import javax.faces.event.PostValidateEvent;
+import javax.faces.view.facelets.ComponentHandler;
+import javax.faces.view.facelets.FaceletContext;
+import javax.faces.view.facelets.TagAttribute;
+import javax.faces.view.facelets.TagConfig;
+import javax.faces.view.facelets.TagHandler;
+
+import org.omnifaces.util.Callback;
+import org.omnifaces.util.Faces;
+
+/**
+ * <p>
+ * <code>&lt;o:viewParamValidationFailed&gt;</code> allows the developer to handle a view parameter validation failure
+ * with either a redirect or a HTTP error status, optionally with respectively a flash message or HTTP error message.
+ * This tag can be placed inside <code>&lt;f:metadata&gt;</code> or <code>&lt;f|o:viewParam&gt;</code>. When placed in
+ * <code>&lt;f|o:viewParam&gt;</code>, then it will be applied when the particular view parameter has a validation
+ * error as per {@link UIViewParameter#isValid()}. When placed in <code>&lt;f:metadata&gt;</code>, and no one view
+ * parameter has already handled the validation error via its own <code>&lt;o:viewParamValidationFailed&gt;</code>,
+ * then it will be applied when there's a general validation error as per {@link FacesContext#isValidationFailed()}.
+ * <p>
+ * The <code>sendRedirect</code> attribute uses under the covers {@link Faces#redirect(String, String...)} to send the
+ * redirect, so the same rules as to scheme and leading slash apply here.
+ * The <code>sendError</code> attribute uses under the covers {@link Faces#responseSendError(int, String)} to send the
+ * error, so you can customize HTTP error pages via <code>&lt;error-page&gt;</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, the <code>&lt;f:viewParam required="true"&gt;</code> has in current Mojarra and MyFaces releases
+ * (as of now, Mojarra 2.2.7 and MyFaces 2.2.4) a design error. When the parameter is not specified in the query string,
+ * then it is retrieved as <code>null</code> which causes that an internal <code>isRequired()</code> check is performed
+ * instead of delegating the check to standard <code>UIInput</code> implementation. This has the consequence that
+ * <code>PreValidateEvent</code> and <code>PostValidateEvent</code> listeners are never invoked, which the
+ * <code>&lt;o:viewParamValidationFailed&gt;</code> is actually relying on. This is fixed in
+ * <code>&lt;o:viewParam</code>.
+ *
+ * <h3>Examples</h3>
+ * <p>
+ * With the example below, when at least one view param is absent, then the client will be returned a HTTP 400 error.
+ * <pre>
+ * &lt;f:metadata&gt;
+ * &lt;o:viewParam name="foo" required="true" /&gt;
+ * &lt;o:viewParam name="bar" required="true" /&gt;
+ * &lt;o:viewParamValidationFailed sendError="400" /&gt;
+ * &lt;/f:metadata&gt;
+ * </pre>
+ * <p>
+ * With the example below, only when the "foo" parameter is absent, then the client will be redirected to "login.xhtml".
+ * When 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>
+ * &lt;f:metadata&gt;
+ * &lt;o:viewParam name="foo" required="true"&gt;
+ * &lt;o:viewParamValidationFailed sendRedirect="login.xhtml" /&gt;
+ * &lt;/o:viewParam&gt;
+ * &lt;o:viewParam name="bar" required="true" /&gt;
+ * &lt;/f:metadata&gt;
+ * </pre>
+ * <p>
+ * With the example below, only when the "foo" parameter is absent, regardless of the "bar" or "baz" parameters, then
+ * the client will be returned a HTTP 401 error. When the "foo" parameter is present, but either "bar" or "baz"
+ * parameter is absent, then the client will be redirected to "search.xhtml".
+ * <pre>
+ * &lt;f:metadata&gt;
+ * &lt;o:viewParam name="foo" required="true"&gt;
+ * &lt;o:viewParamValidationFailed sendError="401" /&gt;
+ * &lt;/o:viewParam&gt;
+ * &lt;o:viewParam name="bar" required="true" /&gt;
+ * &lt;o:viewParam name="baz" required="true" /&gt;
+ * &lt;o:viewParamValidationFailed sendRedirect="search.xhtml" /&gt;
+ * &lt;/f:metadata&gt;
+ * </pre>
+ * <p>
+ * In a nutshell: when there are multiple <code>&lt;o:viewParamValidationFailed&gt;</code> tags, then 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>&lt;f|o:viewParam&gt;</code> takes precedence over the one nested in <code>&lt;f:metadata&gt;</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, then it will be set
+ * as a global flash error message. When <code>sendError</code> is used, then 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 on
+ * both <code>sendRedirect</code> and <code>sendError</code>.
+ * <pre>
+ * &lt;o:viewParamValidationFailed sendRedirect="search.xhtml" message="You need to perform a search." /&gt;
+ * ...
+ * &lt;o:viewParamValidationFailed sendError="401" message="Authentication failed. You need to login." /&gt;
+ * </pre>
+ *
+ * <h3>Design notes</h3>
+ * <p>
+ * You can technically nest multiple <code>&lt;o:viewParamValidationFailed&gt;</code> inside the same parent, but this
+ * is not the documented approach and the behavior is unspecified.
+ * <p>
+ * You can <strong>not</strong> change the HTTP status code of a redirect. This is not a JSF limitation, but a HTTP
+ * limitation. The status code of a redirect will <strong>always</strong> end up 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>&lt;error-page&gt;</code> in <code>web.xml</code>.
+ *
+ * @author Bauke Scholtz
+ * @since 2.0
+ */
+public class ViewParamValidationFailed extends TagHandler implements ComponentSystemEventListener {
+
+ // 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 #processEvent(ComponentSystemEvent)} method after validation.
+ * @throws IllegalArgumentException When the parent component is not an instance of {@link UIViewRoot} or
+ * {@link UIViewParameter}, or when both <code>sendRedirect</code> and <code>sendError</code> attributes are
+ * missing or simultaneously specified, you can specify only one of them.
+ */
+ @Override
+ public void apply(FaceletContext context, final UIComponent parent) throws IOException {
+ if (!(parent instanceof UIViewRoot || parent instanceof UIViewParameter)) {
+ throw new IllegalArgumentException(String.format(ERROR_INVALID_PARENT, this, parent.getClass().getName()));
+ }
+
+ FacesContext facesContext = context.getFacesContext();
+
+ if (!ComponentHandler.isNew(parent) || facesContext.isPostback() || facesContext.getRenderResponse()) {
+ return;
+ }
+
+ sendRedirect = getValueExpression(context, "sendRedirect");
+ sendError = getValueExpression(context, "sendError");
+
+ if (sendRedirect == null && sendError == null) {
+ throw new IllegalArgumentException(String.format(ERROR_MISSING_ATTRIBUTE, this));
+ }
+ else if (sendRedirect != null && sendError != null) {
+ throw new IllegalArgumentException(String.format(ERROR_DOUBLE_ATTRIBUTE, this));
+ }
+
+ message = getValueExpression(context, "message");
+ parent.subscribeToEvent(PostValidateEvent.class, this);
+ }
+
+ /**
+ * 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.
+ * @throws IllegalArgumentException When the <code>sendError</code> attribute does not represent a valid 3-digit
+ * HTTP status code.
+ */
+ @Override
+ public void processEvent(ComponentSystemEvent event) throws AbortProcessingException {
+ if (!(event instanceof PostValidateEvent)) {
+ return; // Should never occur, but you never know.
+ }
+
+ final FacesContext context = getContext();
+ final UIComponent component = event.getComponent();
+
+ if (component instanceof UIViewParameter ? ((UIViewParameter) component).isValid() : !context.isValidationFailed()) {
+ return; // Validation has not failed.
+ }
+
+ if (context.getAttributes().put(getClass().getName(), true) != null) {
+ return; // Validation fail has already been handled. 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.
+ );
+
+ evaluateAttributesAndHandleSendRedirectOrError(context, firstFacesMessage);
+ addCallbackAfterPhaseListener(context.getCurrentPhaseId(), new Callback.Void() {
+ @Override
+ public void invoke() {
+ // We can't unsubscribe immediately inside processEvent() itself, as it would otherwise end up in a
+ // concurrent modification exception while JSF is iterating over all system event listeners.
+ // The unsubscribe is necessary in order to avoid InstantiationException on this tag during restore
+ // view of a postback, because ComponentSystemEventListener instances are also saved in JSF view state.
+ component.unsubscribeFromEvent(PostValidateEvent.class, ViewParamValidationFailed.this);
+ }
+ });
+ }
+
+ 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 (isEmpty(evaluatedMessage)) {
+ 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(
+ String.format(ERROR_INVALID_SENDERROR, sendError, evaluatedSendError));
+ }
+
+ responseSendError(context, Integer.valueOf(evaluatedSendError), evaluatedMessage);
+ }
+ }
+ catch (IOException e) {
+ throw new FacesException(e);
+ }
+ }
+
+ // Helpers --------------------------------------------------------------------------------------------------------
+
+ /**
+ * Get the value of the tag attribute associated with the given attribute name as a value expression.
+ */
+ private ValueExpression getValueExpression(FaceletContext context, String attributeName) {
+ TagAttribute attribute = getAttribute(attributeName);
+ return (attribute != null) ? attribute.getValueExpression(context, Object.class) : null;
+ }
+
+ /**
+ * 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(String.format(ERROR_REQUIRED_ATTRIBUTE, expression));
+ }
+
+ return (value != null) ? value.toString() : null;
+ }
+
+}
Oops, something went wrong.

0 comments on commit ca1e625

Please sign in to comment.