/*
* 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.component.input;
import static java.lang.Boolean.FALSE;
import static javax.servlet.RequestDispatcher.ERROR_REQUEST_URI;
import static org.omnifaces.component.input.Form.PropertyKeys.includeRequestParams;
import static org.omnifaces.component.input.Form.PropertyKeys.includeViewParams;
import static org.omnifaces.component.input.Form.PropertyKeys.useRequestURI;
import static org.omnifaces.util.Components.getParams;
import static org.omnifaces.util.FacesLocal.getRequestAttribute;
import static org.omnifaces.util.FacesLocal.getRequestContextPath;
import static org.omnifaces.util.FacesLocal.getRequestURI;
import static org.omnifaces.util.Servlets.toQueryString;
import static org.omnifaces.util.Utils.isEmpty;
import java.io.IOException;
import javax.faces.application.Application;
import javax.faces.application.ApplicationWrapper;
import javax.faces.application.ViewHandler;
import javax.faces.application.ViewHandlerWrapper;
import javax.faces.component.FacesComponent;
import javax.faces.component.UICommand;
import javax.faces.component.UIForm;
import javax.faces.component.UIViewParameter;
import javax.faces.component.html.HtmlForm;
import javax.faces.context.FacesContext;
import javax.faces.context.FacesContextWrapper;
import org.omnifaces.taghandler.IgnoreValidationFailed;
import org.omnifaces.util.State;
/**
* <p>
* The <code><o:form></code> is a component that extends the standard <code><h:form></code> and provides a
* way to keep view or request parameters in the request URL after a post-back and offers in combination with the
* <code><o:ignoreValidationFailed></code> tag on an {@link UICommand} component the possibility to ignore
* validation failures so that the invoke action phase will be executed anyway. This component also supports adding
* query string parameters to the action URL via nested <code><f:param></code> and <code><o:param></code>.
* <p>
* You can use it the same way as <code><h:form></code>, you only need to change <code>h:</code> to
* <code>o:</code>.
*
* <h3>Include View Params</h3>
* <p>
* The standard {@link UIForm} doesn't put the original view parameters in the action URL that's used for the post-back.
* Instead, it relies on those view parameters to be stored in the state associated with the standard
* {@link UIViewParameter}. Via this state those parameters are invisibly re-applied after every post-back.
* <p>
* The disadvantage of this invisible retention of view parameters is that the user doesn't see them anymore in the
* address bar of the browser that is used to interact with the faces application. Copy-pasting the URL from the address
* bar or refreshing the page by hitting enter inside the address bar will therefore not always yield the expected
* results.
* <p>
* To solve this, this component offers an attribute <code>includeViewParams="true"</code> that will optionally include
* all view parameters, in exactly the same way that this can be done for <code><h:link></code> and
* <code><h:button></code>.
* <pre>
* <o:form includeViewParams="true">
* </pre>
* <p>
* This setting is ignored when <code>includeRequestParams="true"</code> or <code>useRequestURI="true"</code> is used.
*
* <h3>Include Request Params</h3>
* <p>
* As an alternative to <code>includeViewParams</code>, you can use <code>includeRequestParams="true"</code> to
* optionally include the current GET request query string.
* <pre>
* <o:form includeRequestParams="true">
* </pre>
* <p>
* This setting overrides the <code>includeViewParams</code>.
* This setting is ignored when <code>useRequestURI="true"</code> is used.
*
* <h3>Use request URI</h3>
* <p>
* As an alternative to <code>includeViewParams</code> and <code>includeRequestParams</code>, you can use
* <code>useRequestURI="true"</code> to use the current request URI, including with the GET request query string, if
* any. This is particularly useful if you're using FacesViews or forwarding everything to 1 page. Otherwise, by default
* the current view ID will be used.
* <pre>
* <o:form useRequestURI="true">
* </pre>
* <p>
* This setting overrides the <code>includeViewParams</code> and <code>includeRequestParams</code>.
*
* <h3>Add query string parameters to action URL</h3>
* <p>
* The standard {@link UIForm} doesn't support adding query string parameters to the action URL. This component offers
* this possibility via nested <code><f:param></code> and <code><o:param></code>.
* <pre>
* <o:form>
* <f:param name="somename" value="somevalue" />
* ...
* </o:form>
* </pre>
* <p>
* This can be used in combination with <code>useRequestURI</code>, <code>includeViewParams</code> and
* <code>includeRequestParams</code>. The <code><f|o:param></code> will override any included view or request
* parameters on the same name. To conditionally add or override, use the <code>disabled</code> attribute of
* <code><f|o:param></code>.
* <p>
* The support was added in OmniFaces 2.2.
*
* <h3>Ignore Validation Failed</h3>
* <p>
* In order to properly use the <code><o:ignoreValidationFailed></code> tag on an {@link UICommand} component, its
* parent <code><h:form></code> component has to be replaced by this <code><o:form></code> component.
* See also {@link IgnoreValidationFailed}.
*
* @since 1.1
* @author Arjan Tijms
* @author Bauke Scholtz
*/
@FacesComponent(Form.COMPONENT_TYPE)
public class Form extends HtmlForm {
// Constants ------------------------------------------------------------------------------------------------------
public static final String COMPONENT_TYPE = "org.omnifaces.component.input.Form";
enum PropertyKeys {
includeViewParams,
includeRequestParams,
useRequestURI
}
// Variables ------------------------------------------------------------------------------------------------------
private final State state = new State(getStateHelper());
private boolean ignoreValidationFailed;
// Actions --------------------------------------------------------------------------------------------------------
@Override
public void processValidators(FacesContext context) {
if (isIgnoreValidationFailed()) {
super.processValidators(new IgnoreValidationFailedFacesContext(context));
}
else {
super.processValidators(context);
}
}
@Override
public void processUpdates(FacesContext context) {
if (isIgnoreValidationFailed()) {
super.processUpdates(new IgnoreValidationFailedFacesContext(context));
}
else {
super.processUpdates(context);
}
}
@Override
public void encodeBegin(FacesContext context) throws IOException {
super.encodeBegin(new ActionURLDecorator(context, this));
}
// Getters/setters ------------------------------------------------------------------------------------------------
/**
* Returns whether or not the view parameters should be encoded into the form's action URL.
* @return Whether or not the view parameters should be encoded into the form's action URL.
*/
public boolean isIncludeViewParams() {
return state.get(includeViewParams, FALSE);
}
/**
* Set whether or not the view parameters should be encoded into the form's action URL.
*
* @param includeViewParams
* The state of the switch for encoding view parameters
*/
public void setIncludeViewParams(boolean includeViewParams) {
state.put(PropertyKeys.includeViewParams, includeViewParams);
}
/**
* Returns whether or not the request parameters should be encoded into the form's action URL.
* @return Whether or not the request parameters should be encoded into the form's action URL.
* @since 1.5
*/
public boolean isIncludeRequestParams() {
return state.get(includeRequestParams, FALSE);
}
/**
* Set whether or not the request parameters should be encoded into the form's action URL.
*
* @param includeRequestParams
* The state of the switch for encoding request parameters.
* @since 1.5
*/
public void setIncludeRequestParams(boolean includeRequestParams) {
state.put(PropertyKeys.includeRequestParams, includeRequestParams);
}
/**
* Returns whether or not the request URI should be used as form's action URL.
* @return Whether or not the request URI should be used as form's action URL.
* @since 1.6
*/
public boolean isUseRequestURI() {
return state.get(useRequestURI, FALSE);
}
/**
* Set whether or not the request URI should be used as form's action URL.
*
* @param useRequestURI
* The state of the switch for using request URI.
* @since 1.6
*/
public void setUseRequestURI(boolean useRequestURI) {
state.put(PropertyKeys.useRequestURI, useRequestURI);
}
/**
* Returns whether or not the form should ignore validation fail (and thus proceed to update model/invoke action).
* @return Whether or not the form should ignore validation fail.
* @since 2.1
*/
public boolean isIgnoreValidationFailed() {
return ignoreValidationFailed;
}
/**
* Set whether or not the form should ignore validation fail.
* @param ignoreValidationFailed Whether or not the form should ignore validation fail.
* @since 2.1
*/
public void setIgnoreValidationFailed(boolean ignoreValidationFailed) {
this.ignoreValidationFailed = ignoreValidationFailed;
}
// Nested classes -------------------------------------------------------------------------------------------------
/**
* FacesContext wrapper which performs NOOP during {@link FacesContext#validationFailed()} and
* {@link FacesContext#renderResponse()}.
*
* @author Bauke Scholtz
*/
private static class IgnoreValidationFailedFacesContext extends FacesContextWrapper {
private FacesContext wrapped;
public IgnoreValidationFailedFacesContext(FacesContext wrapped) {
this.wrapped = wrapped;
}
@Override
public void validationFailed() {
// NOOP.
}
@Override
public void renderResponse() {
// NOOP.
}
@Override
public FacesContext getWrapped() {
return wrapped;
}
}
/**
* Helper class used for creating a FacesContext with a decorated FacesContext -> Application -> ViewHandler
* -> getActionURL.
*
* @author Arjan Tijms
*/
private static class ActionURLDecorator extends FacesContextWrapper {
private FacesContext wrapped;
private Form form;
public ActionURLDecorator(FacesContext wrapped, Form form) {
this.wrapped = wrapped;
this.form = form;
}
@Override
public Application getApplication() {
return new ActionURLDecoratorApplication(getWrapped().getApplication(), form);
}
@Override
public FacesContext getWrapped() {
return wrapped;
}
}
private static class ActionURLDecoratorApplication extends ApplicationWrapper {
private Application wrapped;
private Form form;
public ActionURLDecoratorApplication(Application wrapped, Form form) {
this.wrapped = wrapped;
this.form = form;
}
@Override
public ViewHandler getViewHandler() {
return new ActionURLDecoratorViewHandler(getWrapped().getViewHandler(), form);
}
@Override
public Application getWrapped() {
return wrapped;
}
}
private static class ActionURLDecoratorViewHandler extends ViewHandlerWrapper {
private ViewHandler wrapped;
private Form form;
public ActionURLDecoratorViewHandler(ViewHandler wrapped, Form form) {
this.wrapped = wrapped;
this.form = form;
}
/**
* The actual method we're decorating in order to either include the view parameters into the
* action URL, or include the request parameters into the action URL, or use request URI as
* action URL. Any <code><f|o:param></code> nested in the form component will be included
* in the query string, overriding any existing view or request parameters on same name.
*/
@Override
public String getActionURL(FacesContext context, String viewId) {
String actionURL = form.isUseRequestURI() ? getActionURL(context) : getWrapped().getActionURL(context, viewId);
String queryString = toQueryString(getParams(form, form.isUseRequestURI() || form.isIncludeRequestParams(), form.isIncludeViewParams()));
return isEmpty(queryString) ? actionURL : (actionURL + (actionURL.contains("?") ? "&" : "?") + queryString);
}
private String getActionURL(FacesContext context) {
String actionURL = (getRequestAttribute(context, ERROR_REQUEST_URI) != null) ? getRequestContextPath(context) : getRequestURI(context);
return actionURL.isEmpty() ? "/" : actionURL;
}
@Override
public ViewHandler getWrapped() {
return wrapped;
}
}
}