/
ValidateUniqueColumn.java
280 lines (235 loc) · 10.3 KB
/
ValidateUniqueColumn.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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
/*
* Copyright 2021 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
*
* https://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.parseBoolean;
import static java.lang.String.format;
import static org.omnifaces.util.Components.getClosestParent;
import static org.omnifaces.util.Components.getLabel;
import static org.omnifaces.util.Faces.getELContext;
import static org.omnifaces.util.Messages.addError;
import java.io.IOException;
import javax.el.ValueExpression;
import javax.faces.component.UIComponent;
import javax.faces.component.UIData;
import javax.faces.component.UIInput;
import javax.faces.component.visit.VisitCallback;
import javax.faces.component.visit.VisitContext;
import javax.faces.component.visit.VisitResult;
import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;
import javax.faces.event.ValueChangeListener;
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;
/**
* <p>
* The <code><o:validateUniqueColumn></code> validates if the given {@link UIInput} component in an {@link UIData}
* component has an unique value throughout all rows, also those not visible by pagination. This validator works
* directly on the data model and may therefore not work as expected if the data model does not represent
* <strong>all</strong> available rows of the {@link UIData} component (e.g. when there's means of lazy loading).
* <p>
* The default message is
* <blockquote>{0}: Please fill out an unique value for the entire column. Duplicate found in row {1}</blockquote>
*
* <h3>Usage</h3>
* <p>
* Usage example:
* <pre>
* <h:dataTable value="#{bean.items}" var="item">
* <h:column>
* <h:inputText value="#{item.value}">
* <o:validateUniqueColumn />
* </h:inputText>
* </h:column>
* </h:dataTable>
* </pre>
* <p>
* In an invalidating case, only the first row on which the value is actually changed (i.e. the value change event has
* been fired on the input component in the particular row) will be marked invalid and a faces message will be added
* on the client ID of the input component in the particular row. The default message can be changed by the
* <code>message</code> attribute. Any "{0}" placeholder in the message will be substituted with the label of the
* input component. Any "{1}" placeholder in the message will be substituted with the 1-based row index of the data
* model. Note that this does not take pagination into account and that this needs if necessary to be taken care of in
* the custom message yourself.
* <pre>
* <o:validateUniqueColumn message="Duplicate value!" />
* </pre>
*
* @author Bauke Scholtz
* @since 1.3
*/
public class ValidateUniqueColumn extends TagHandler implements ValueChangeListener {
// Private constants ----------------------------------------------------------------------------------------------
private static final String DEFAULT_MESSAGE =
"{0}: Please fill out an unique value for the entire column. Duplicate found in row {1}";
private static final String ERROR_INVALID_PARENT =
"Parent component of o:validateUniqueColumn must be an instance of UIInput. Encountered invalid type '%s'.";
private static final String ERROR_INVALID_PARENT_PARENT =
"Parent component of o:validateUniqueColumn must be enclosed in an UIData component.";
// Properties -----------------------------------------------------------------------------------------------------
private ValueExpression message;
private ValueExpression disabled;
// Constructors ---------------------------------------------------------------------------------------------------
/**
* The tag constructor.
* @param config The tag config.
*/
public ValidateUniqueColumn(TagConfig config) {
super(config);
}
// Actions --------------------------------------------------------------------------------------------------------
/**
* If the component is new, check if it's an instance of {@link UIInput} and then register this tag as a value
* change listener on it. If the component is not new, check if there's an {@link UIData} parent.
*/
@Override
public void apply(FaceletContext context, UIComponent parent) throws IOException {
if (!ComponentHandler.isNew(parent)) {
if (getClosestParent(parent, UIData.class) == null) {
throw new IllegalArgumentException(ERROR_INVALID_PARENT_PARENT);
}
return;
}
if (!(parent instanceof UIInput)) {
throw new IllegalArgumentException(format(ERROR_INVALID_PARENT, parent.getClass().getName()));
}
// Get the tag attributes as value expressions instead of the immediately evaluated values. This allows us to
// re-evaluate them on a per-row basis which in turn allows the developer to use the currently iterated row
// object in the message and/or the disabled attribute.
message = getValueExpression("message", context);
disabled = getValueExpression("disabled", context);
// This validator is registered as a value change listener, which thus does only a full UIData tree visit when
// the value is really changed. If it were a normal validator, then if would have performed the same visit on
// every single row which would have been very inefficient.
((UIInput) parent).addValueChangeListener(this);
}
/**
* Get the value of the tag attribute associated with the given attribute name as a value expression.
*/
private ValueExpression getValueExpression(String attributeName, FaceletContext context) {
TagAttribute attribute = getAttribute(attributeName);
if (attribute != null) {
return attribute.getValueExpression(context, Object.class);
}
return null;
}
/**
* When this tag is not disabled, the input value is changed, the input component is valid and the input component's
* local value is not null, then check for a duplicate value by visiting all rows of the parent {@link UIData}
* component.
*/
@Override
public void processValueChange(ValueChangeEvent event) {
if (isDisabled()) {
return;
}
UIInput input = (UIInput) event.getComponent();
if (!input.isValid() || input.getLocalValue() == null) {
return;
}
UIData table = getClosestParent(input, UIData.class);
int originalRows = table.getRows();
table.setRows(0); // We want to visit all rows.
FacesContext context = FacesContext.getCurrentInstance();
UniqueColumnValueChecker checker = new UniqueColumnValueChecker(table, input);
table.visitTree(VisitContext.createVisitContext(context), checker);
table.setRows(originalRows);
if (checker.isDuplicate()) {
input.setValid(false);
context.validationFailed();
addError(input.getClientId(context), getMessage(), getLabel(input), checker.getDuplicateIndex() + 1);
}
}
// Getters/setters ------------------------------------------------------------------------------------------------
/**
* Returns the runtime evaluated value of the message attribute.
* @return The runtime evaluated value of the message attribute.
*/
public String getMessage() {
return getValue(message, DEFAULT_MESSAGE);
}
/**
* Returns the runtime evaluated value of the disabled attribute.
* @return The runtime evaluated value of the disabled attribute.
*/
public boolean isDisabled() {
if (disabled == null) {
return false;
}
if (disabled.isLiteralText()) {
return parseBoolean(disabled.getExpressionString());
}
return getValue(disabled, false);
}
// Helpers --------------------------------------------------------------------------------------------------------
/**
* Returns the evaluated value of the given value expression, or the given default value if the given value
* expression itself or its evaluated value is <code>null</code>.
* @param expression The value expression to return the value for.
* @param defaultValue The default value to return if the value expression itself or its evaluated value is
* <code>null</code>.
* @return The evaluated value of the given value expression, or the given default value if the given value
* expression itself or its evaluated value is <code>null</code>.
*/
@SuppressWarnings("unchecked")
private static <T> T getValue(ValueExpression expression, T defaultValue) {
if (expression != null) {
T value = (T) expression.getValue(getELContext());
if (value != null) {
return value;
}
}
return defaultValue;
}
// Nested classes -------------------------------------------------------------------------------------------------
/**
* The unique column value checker as tree visit callback.
* @author Bauke Scholtz
*/
private static class UniqueColumnValueChecker implements VisitCallback {
private UIData table;
private int rowIndex;
private UIInput input;
private Object value;
private boolean duplicate;
private int duplicateIndex;
public UniqueColumnValueChecker(UIData table, UIInput input) {
this.table = table;
rowIndex = table.getRowIndex();
this.input = input;
value = input.getLocalValue();
}
@Override
public VisitResult visit(VisitContext context, UIComponent target) {
// Yes, this check does look a bit strange, but really physically the very same single UIInput component is
// been reused in all rows of the UIData component. It's only its internal state which changes on a per-row
// basis, as would happen during the tree visit. Those changes are reflected in the "input" reference.
if (target == input && rowIndex != table.getRowIndex()
&& input.isValid() && value.equals(input.getLocalValue()))
{
duplicate = true;
duplicateIndex = table.getRowIndex();
return VisitResult.COMPLETE;
}
return VisitResult.ACCEPT;
}
public boolean isDuplicate() {
return duplicate;
}
public int getDuplicateIndex() {
return duplicateIndex;
}
}
}