/
Ajax.java
402 lines (351 loc) · 17.2 KB
/
Ajax.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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
/*
* Copyright 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.util;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.omnifaces.util.Components.getCurrentComponent;
import static org.omnifaces.util.Components.getCurrentForm;
import static org.omnifaces.util.Faces.createResource;
import static org.omnifaces.util.Faces.isAjaxRequestWithPartialRendering;
import java.beans.Introspector;
import java.io.IOException;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import jakarta.faces.FacesException;
import jakarta.faces.application.Resource;
import jakarta.faces.component.UIColumn;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIData;
import jakarta.faces.component.UINamingContainer;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.PartialViewContext;
import org.omnifaces.context.OmniPartialViewContext;
import org.omnifaces.context.OmniPartialViewContextFactory;
/**
* <p>
* Collection of utility methods for working with {@link PartialViewContext}. There are also shortcuts to the current
* {@link OmniPartialViewContext} instance.
* <p>
* This utility class allows an easy way of programmaticaly (from inside a managed bean method) specifying new client
* IDs which should be ajax-updated via {@link #update(String...)}, also {@link UIData} rows or columns on specific
* index via {@link #updateRow(UIData, int)} and {@link #updateColumn(UIData, int)}, specifying callback scripts which
* should be executed on complete of the ajax response via {@link #oncomplete(String...)}, and loading new JavaScript
* resources on complete of the ajax response via {@link #load(String, String)}.
* <p>
* It also supports adding arguments to the JavaScript scope via {@link #data(String, Object)}, {@link #data(Map)} and
* {@link #data(Object...)}. The added arguments are during the "on complete" phase as a JSON object available by
* <code>OmniFaces.Ajax.data</code> in JavaScript context. The JSON object is encoded by {@link Json#encode(Object)}
* which supports standard Java types {@link Boolean}, {@link Number}, {@link CharSequence} and {@link Date} arrays,
* {@link Collection}s and {@link Map}s of them and as last resort it will use the {@link Introspector} to examine it
* as a Javabean and encode it like a {@link Map}.
* <p>
* Note that {@link #updateRow(UIData, int)} and {@link #updateColumn(UIData, int)} can only update cell content when
* it has been wrapped in some container component with a fixed ID.
*
* <h2>Usage</h2>
* <p>
* Here are <strong>some</strong> examples:
* <pre>
* // Update specific component on complete of ajax.
* Ajax.update("formId:someId");
* </pre>
* <pre>
* // Load script resource on complete of ajax.
* Ajax.load("libraryName", "js/resourceName.js");
* </pre>
* <pre>
* // Add variables to JavaScript scope.
* Ajax.data("foo", foo); // It will be available as OmniFaces.Ajax.data.foo in JavaScript.
* </pre>
* <pre>
* // Execute script on complete of ajax.
* Ajax.oncomplete("alert(OmniFaces.Ajax.data.foo)");
* </pre>
* <p>
* For a full list, check the <a href="#method.summary">method summary</a>.
*
* @author Bauke Scholtz
* @since 1.2
* @see Json
* @see OmniPartialViewContext
* @see OmniPartialViewContextFactory
*/
public final class Ajax {
// Constants ------------------------------------------------------------------------------------------------------
private static final String ERROR_NO_SCRIPT_RESOURCE =
"";
private static final String ERROR_NO_PARTIAL_RENDERING =
"The current request is not an ajax request with partial rendering."
+ " Use Components#addScriptXxx() methods instead.";
private static final String ERROR_ARGUMENTS_LENGTH =
"The arguments length must be even. Encountered %d items.";
private static final String ERROR_ARGUMENT_TYPE =
"The argument name must be a String. Encountered type '%s' with value '%s'.";
// Constructors ---------------------------------------------------------------------------------------------------
private Ajax() {
// Hide constructor.
}
// Shortcuts ------------------------------------------------------------------------------------------------------
/**
* Returns the current partial view context (the ajax context).
* <p>
* <i>Note that whenever you absolutely need this method to perform a general task, you might want to consider to
* submit a feature request to OmniFaces in order to add a new utility method which performs exactly this general
* task.</i>
* @return The current partial view context.
* @see FacesContext#getPartialViewContext()
*/
public static PartialViewContext getContext() {
return Faces.getContext().getPartialViewContext();
}
/**
* Update the given client IDs in the current ajax response. Note that those client IDs should not start with the
* naming container separator character like <code>:</code>. This method also supports the client ID keywords
* <code>@all</code>, <code>@form</code> and <code>@this</code> which respectively refers the entire view, the
* currently submitted form as obtained by {@link Components#getCurrentForm()} and the currently processed
* component as obtained by {@link UIComponent#getCurrentComponent(FacesContext)}. Any other client ID starting
* with <code>@</code> is by design ignored, including <code>@none</code>.
* @param clientIds The client IDs to be updated in the current ajax response.
* @see PartialViewContext#getRenderIds()
*/
public static void update(String... clientIds) {
PartialViewContext context = getContext();
Collection<String> renderIds = context.getRenderIds();
for (String clientId : clientIds) {
if (clientId.charAt(0) != '@') {
renderIds.add(clientId);
}
else if ("@all".equals(clientId)) {
context.setRenderAll(true);
}
else if ("@form".equals(clientId)) {
UIComponent currentForm = getCurrentForm();
if (currentForm != null) {
renderIds.add(currentForm.getClientId());
}
}
else if ("@this".equals(clientId)) {
UIComponent currentComponent = getCurrentComponent();
if (currentComponent != null) {
renderIds.add(currentComponent.getClientId());
}
}
}
}
/**
* Update the entire view.
* @see PartialViewContext#setRenderAll(boolean)
* @since 1.5
*/
public static void updateAll() {
getContext().setRenderAll(true);
}
/**
* Update the row of the given {@link UIData} component at the given zero-based row index. This will basically
* update all direct children of all {@link UIColumn} components at the given row index.
* <p>
* Note that the to-be-updated direct child of {@link UIColumn} must be a fullworthy Faces UI component which renders
* a concrete HTML element to the output, so that JS/ajax can update it. So if you have due to design restrictions
* for example a <code><h:panelGroup rendered="..."></code> without an ID, then you should give it an ID.
* This way it will render a <code><span id="..."></code> which is updateable by JS/ajax.
* @param table The {@link UIData} component.
* @param index The zero-based index of the row to be updated.
* @since 1.3
*/
public static void updateRow(UIData table, int index) {
if (index < 0 || table.getRowCount() < 1 || index >= table.getRowCount() || table.getChildCount() == 0) {
return;
}
updateRowCells(table, index);
}
private static void updateRowCells(UIData table, int index) {
FacesContext context = FacesContext.getCurrentInstance();
String parentId = table.getParent().getNamingContainer().getClientId(context);
String tableId = table.getId();
char separator = UINamingContainer.getSeparatorChar(context);
Collection<String> renderIds = getContext().getRenderIds();
for (UIComponent column : table.getChildren()) {
if (column instanceof UIColumn) {
if (!column.isRendered()) {
continue;
}
for (UIComponent cell : column.getChildren()) {
if (!cell.isRendered()) {
continue;
}
renderIds.add(format("%s%c%s%c%d%c%s", parentId, separator, tableId, separator, index, separator, cell.getId()));
}
}
else if (column instanceof UIData) { // <p:columns>.
updateRowCells((UIData) column, renderIds, tableId, index, separator);
}
}
}
private static void updateRowCells(UIData columns, Collection<String> renderIds, String tableId, int index, char separator) {
String columnId = columns.getId();
int columnCount = columns.getRowCount();
for (UIComponent cell : columns.getChildren()) {
if (!cell.isRendered()) {
continue;
}
for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) {
renderIds.add(format("%s%c%d%c%s%c%d%c%s", tableId, separator, index, separator, columnId, separator, columnIndex, separator, cell.getId()));
}
}
}
/**
* Update the column of the given {@link UIData} component at the given zero-based column index. This will basically
* update all direct children of the {@link UIColumn} component at the given column index in all rows. The column
* index is the physical column index and does not depend on whether one or more columns is rendered or not (i.e. it
* is not necessarily the same column index as the enduser sees in the UI).
* <p>
* Note that the to-be-updated direct child of {@link UIColumn} must be a fullworthy Faces UI component which renders
* a concrete HTML element to the output, so that JS/ajax can update it. So if you have due to design restrictions
* for example a <code><h:panelGroup rendered="..."></code> without an ID, then you should give it an ID.
* This way it will render a <code><span id="..."></code> which is updateable by JS/ajax.
* @param table The {@link UIData} component.
* @param index The zero-based index of the column to be updated.
* @since 1.3
*/
public static void updateColumn(UIData table, int index) {
if (index < 0 || table.getRowCount() < 1 || index > table.getChildCount()) {
return;
}
int rowCount = (table.getRows() == 0) ? table.getRowCount() : table.getRows();
if (rowCount == 0) {
return;
}
updateColumnCells(table, index, rowCount);
}
private static void updateColumnCells(UIData table, int index, int rowCount) {
FacesContext context = FacesContext.getCurrentInstance();
String parentId = table.getParent().getNamingContainer().getClientId(context);
String tableId = table.getId();
char separator = UINamingContainer.getSeparatorChar(context);
Collection<String> renderIds = getContext().getRenderIds();
UIColumn column = findColumn(table, index);
if (column != null && column.isRendered()) {
for (UIComponent cell : column.getChildren()) {
if (!cell.isRendered()) {
continue;
}
String cellId = cell.getId();
for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) {
renderIds.add(format("%s%c%s%c%d%c%s", parentId, separator, tableId, separator, rowIndex, separator, cellId));
}
}
}
}
private static UIColumn findColumn(UIData table, int index) {
int columnIndex = 0;
for (UIComponent column : table.getChildren()) {
if (column instanceof UIColumn && columnIndex++ == index) {
return (UIColumn) column;
}
}
return null;
}
/**
* Load given script resource on complete of the current ajax response. Basically, it loads the script resource as
* a {@link String} and then delegates it to {@link #oncomplete(String...)}.
* @param libraryName Library name of the JavaScript resource.
* @param resourceName Resource name of the JavaScript resource.
* @throws IllegalArgumentException When given script resource cannot be found.
* @throws IllegalStateException When current request is not an ajax request with partial rendering. You should use
* {@link Components#addScriptResource(String, String)} instead.
* @since 2.3
*/
public static void load(String libraryName, String resourceName) {
Resource resource = createResource(libraryName, resourceName);
if (resource == null) {
throw new IllegalArgumentException(ERROR_NO_SCRIPT_RESOURCE);
}
try (Scanner scanner = new Scanner(resource.getInputStream(), UTF_8.name())) {
oncomplete(scanner.useDelimiter("\\A").next());
}
catch (IOException e) {
throw new FacesException(e);
}
}
/**
* Execute the given scripts on complete of the current ajax response.
* @param scripts The scripts to be executed.
* @throws IllegalStateException When current request is not an ajax request with partial rendering. You should use
* {@link Components#addScript(String)} instead.
* @see OmniPartialViewContext#addCallbackScript(String)
*/
public static void oncomplete(String... scripts) {
if (!isAjaxRequestWithPartialRendering()) {
throw new IllegalStateException(ERROR_NO_PARTIAL_RENDERING);
}
OmniPartialViewContext context = OmniPartialViewContext.getCurrentInstance();
for (String script : scripts) {
context.addCallbackScript(script);
}
}
/**
* Add the given data argument to the current ajax response. They are as JSON object available by
* <code>OmniFaces.Ajax.data</code>.
* @param name The argument name.
* @param value The argument value.
* @see OmniPartialViewContext#addArgument(String, Object)
*/
public static void data(String name, Object value) {
OmniPartialViewContext.getCurrentInstance().addArgument(name, value);
}
/**
* Add the given data arguments to the current ajax response. The arguments length must be even. Every first and
* second argument is considered the name and value pair. The name must always be a {@link String}. They are as JSON
* object available by <code>OmniFaces.Ajax.data</code>.
* @param namesValues The argument names and values.
* @throws IllegalArgumentException When the arguments length is not even, or when a name is not a string.
* @see OmniPartialViewContext#addArgument(String, Object)
*/
public static void data(Object... namesValues) {
if (namesValues.length % 2 != 0) {
throw new IllegalArgumentException(format(ERROR_ARGUMENTS_LENGTH, namesValues.length));
}
OmniPartialViewContext context = OmniPartialViewContext.getCurrentInstance();
for (int i = 0; i < namesValues.length; i+= 2) {
if (!(namesValues[i] instanceof String)) {
String type = (namesValues[i]) != null ? namesValues[i].getClass().getName() : "null";
throw new IllegalArgumentException(format(ERROR_ARGUMENT_TYPE, type, namesValues[i]));
}
context.addArgument((String) namesValues[i], namesValues[i + 1]);
}
}
/**
* Add the given mapping of data arguments to the current ajax response. They are as JSON object available by
* <code>OmniFaces.Ajax.data</code>.
* @param data The mapping of data arguments.
* @see OmniPartialViewContext#addArgument(String, Object)
*/
public static void data(Map<String, Object> data) {
OmniPartialViewContext context = OmniPartialViewContext.getCurrentInstance();
for (Entry<String, Object> entry : data.entrySet()) {
context.addArgument(entry.getKey(), entry.getValue());
}
}
/**
* Returns <code>true</code> if the given client ID was executed in the current ajax request.
* @param clientId The client ID to be checked.
* @return <code>true</code> if the given client ID was executed in the current ajax request.
* @since 3.6
*/
public static boolean isExecuted(String clientId) {
return getContext().getExecuteIds().contains(clientId);
}
}