/
ViewParam.java
216 lines (182 loc) · 8.67 KB
/
ViewParam.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
/*
* 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;
}
}
}