/* * 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 org.omnifaces.util.Faces.getELContext; import static org.omnifaces.util.Faces.isPostback; import java.util.Map; import javax.el.ValueExpression; import javax.faces.component.FacesComponent; import javax.faces.component.UIInput; import javax.faces.component.UIViewParameter; import javax.faces.context.FacesContext; import javax.faces.event.PostValidateEvent; import javax.faces.event.PreValidateEvent; import org.omnifaces.util.MapWrapper; import org.omnifaces.util.Utils; /** * <p> * The <code><o:viewParam></code> is a component that extends the standard <code><f:viewParam></code> and * provides a stateless mode of operation and fixes the issue wherein null model values are converted to empty string * parameters in query string (e.g. when <code>includeViewParams=true</code>) and the (bean) validation never being * triggered when the parameter is completely absent in query string, causing e.g. <code>@NotNull</code> to fail. * * <h3>Stateless mode to avoid unnecessary conversion, validation and model updating on postbacks</h3> * <p> * The standard {@link UIViewParameter} implementation calls the model setter again after postback. This is not always * desired when being bound to a view scoped bean and can lead to performance problems when combined with an expensive * converter. To solve this, this component by default stores the submitted value as a component property instead of in * the model (and thus in the view state in case the binding is to a view scoped bean). * <p> * The standard {@link UIViewParameter} implementation calls the converter and validators again on postbacks. This is * not always desired when you have e.g. a <code>required="true"</code>, but the parameter is not retained on form * submit. You would need to retain it on every single command link/button by <code><f:param></code>. To solve * this, this component doesn't call the converter and validators again on postbacks. * * <h3>Using name as default for label</h3> * <p> * The <code><o:viewParam></code> also provides a default for the <code>label</code> atrribute. When the * <code>label</code> attribute is omitted, the <code>name</code> attribute will be used as label. * * <h3>Avoid unnecessary empty parameter in query string</h3> * <p> * The standard {@link UIViewParameter} implementation calls the converter regardless of whether the evaluated model * value is <code>null</code> or not. As converters by specification return an empty string in case of <code>null</code> * value, this is being added to the query string as an empty parameter when e.g. <code>includeViewParams=true</code> is * used. This is not desired. The workaround was added in OmniFaces 1.8. * * <h3>Support bean validation and triggering validate events on null value</h3> * <p> * The standard {@link UIViewParameter} implementation uses in JSF 2.0-2.2 an internal "is required" check when the * submitted value is <code>null</code>, hereby completely bypassing the standard {@link UIInput} validation, including * any bean validation annotations and even the {@link PreValidateEvent} and {@link PostValidateEvent} events. This is * not desired. The workaround was added in OmniFaces 2.0. In JSF 2.3, this has been fixed and has only effect when * <code>javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</code> context param is set to <code>true</code>. * * <h3>Default value when no parameter is set</h3> * <p> * The <code><o:viewParam></code> also supports providing a default value via the new <code>default</code> * attribute. When the parameter is not available, then the value specified in <code>default</code> attribute will be * set in the model instead. The support was added in OmniFaces 2.2. * * <h3>Usage</h3> * <p> * You can use it the same way as <code><f:viewParam></code>, you only need to change <code>f:</code> to * <code>o:</code>. * <pre> * <o:viewParam name="foo" value="#{bean.foo}" /> * </pre> * * @author Arjan Tijms * @author Bauke Scholtz */ @FacesComponent(ViewParam.COMPONENT_TYPE) public class ViewParam extends UIViewParameter { // Public constants ----------------------------------------------------------------------------------------------- public static final String COMPONENT_TYPE = "org.omnifaces.component.input.ViewParam"; // Variables ------------------------------------------------------------------------------------------------------ private String submittedValue; private Map<String, Object> attributeInterceptMap; // Actions -------------------------------------------------------------------------------------------------------- @Override public void processDecodes(FacesContext context) { // Ignore any request parameters that are present when the postback is done. if (!context.isPostback()) { super.processDecodes(context); } } @Override public void processValidators(FacesContext context) { if (!context.isPostback()) { if (isEmpty(getSubmittedValue())) { setSubmittedValue(getDefault()); } if (getSubmittedValue() == null) { setSubmittedValue(""); // Workaround for it never triggering the (bean) validation when unspecified. } super.processValidators(context); } } @Override public Map<String, Object> getAttributes() { if (attributeInterceptMap == null) { attributeInterceptMap = new AttributeInterceptMap(super.getAttributes()); } return attributeInterceptMap; } /** * When there's a value expression and the evaluated model value is <code>null</code>, then just return * <code>null</code> instead of delegating to default implementation which would return an empty string when a * converter is attached. * @since 1.8 */ @Override public String getStringValueFromModel(FacesContext context) { ValueExpression ve = getValueExpression("value"); Object value = (ve != null) ? ve.getValue(context.getELContext()) : null; return (value != null) ? super.getStringValueFromModel(context) : null; } // Attribute getters/setters -------------------------------------------------------------------------------------- @Override public String getSubmittedValue() { return submittedValue; } @Override public void setSubmittedValue(Object submittedValue) { this.submittedValue = (String) submittedValue; // Don't delegate to statehelper to keep it stateless. } /** * Returns the default value in case the actual request parameter is <code>null</code> or empty. * @return The default value in case the actual request parameter is <code>null</code> or empty. * @since 2.2 */ public String getDefault() { return (String) getStateHelper().eval("default"); } /** * Sets the default value in case the actual request parameter is <code>null</code> or empty. * @param defaultValue The default value in case the actual request parameter is <code>null</code> or empty. * @since 2.2 */ public void setDefault(String defaultValue) { getStateHelper().put("default", defaultValue); } @Override public boolean isRequired() { // The request parameter is ignored on postbacks, however it's already present in the view scoped bean. // So we can safely skip the required validation on postbacks. return !isPostback() && super.isRequired(); } // Inner classes -------------------------------------------------------------------------------------------------- private class AttributeInterceptMap extends MapWrapper<String, Object> { private static final long serialVersionUID = -7674000948288609007L; private AttributeInterceptMap(Map<String, Object> map) { super(map); } @Override public Object get(Object key) { Object value = super.get(key); if (Utils.isEmpty(value) && "label".equals(key)) { // Next check if our outer component has a value expression for the label ValueExpression labelExpression = ViewParam.this.getValueExpression("label"); if (labelExpression != null) { value = labelExpression.getValue(getELContext()); } // No explicit label defined, default to "name" (which is in many cases the most sane label anyway). if (value == null) { value = ViewParam.this.getName(); } } return value; } } }