1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.myfaces.trinidad.model;
20
21 import java.util.HashMap;
22 import java.util.Map;
23
24 import javax.faces.context.ExternalContext;
25 import javax.faces.context.FacesContext;
26
27 /**
28 * <p>
29 * There are two common scenarios for processes,
30 * "Plus One" and "Max Visited" which are explained below.
31 * <ul>
32 * <li>"Plus One" - from the current step the user can
33 * navigate to any previous page and the next page.
34 * If the user is on the 5th step of a process
35 * and goes back to step 2, then the user can only
36 * navigate from step 2 to step 1 and step 3.
37 * </li>
38 * <li>"Max Visited" - the user can navigate to the max
39 * visited page. If the user is currently on the max
40 * visited page then the user can also navigate to
41 * the next page. If the user is on the 5th step of a
42 * process and goes back to step 2, then the user can
43 * navigate from step 2 to steps 1, 2, 3, 4, and 5.
44 * </li>
45 * </ul>
46 *
47 * </p>
48 * <p>
49 * A node in a process should be readOnly
50 * if that step of the process is not reachable from the current
51 * step.
52 * </p>
53 * <p>
54 * A node in a process should be immediate if the values
55 * in the current step don't need to be validated.
56 * </p>
57 * <p>
58 */
59 public class ProcessUtils
60 {
61
62 //**********************************
63 // Plus One Process methods
64 //**********************************
65 /**
66 *
67 * <p>
68 * Determines immediate for a "plus one" process.
69 * Immediate will be true for any previous step, and false otherwise.
70 * For example if the user is on step 5
71 * and goes back to step 2, the user will have to come back
72 * to step 5 again, so the fields on page 5 don't need to
73 * be validated when going back to steps 1,2,3,4, but should be
74 * validated when going to step 6.
75 * </p>
76 *
77 *
78 * @param model the model instance to use.
79 * When the model is passed in a call to model.getPath should return the
80 * path for the "current" node.
81 * @param defaultReturnValue if the current and focus nodes aren't
82 * siblings in the tree, this value will be returned.
83 * @return whether or not the current node should be immediate.
84 */
85 public static boolean isImmediate(
86 MenuModel model,
87 boolean defaultReturnValue
88 )
89 {
90 Object focusPath = model.getFocusRowKey();
91 Object currPath = model.getRowKey();
92 boolean returnDefault = _hasDifferentAncestors(model,
93 focusPath,
94 currPath);
95
96 if (returnDefault)
97 {
98 model.setRowKey(currPath);
99 return defaultReturnValue;
100 }
101
102 model.setRowKey(focusPath);
103 int focusIndex = model.getRowIndex();
104 model.setRowKey(currPath);
105 int currIndex = model.getRowIndex();
106
107 if (currIndex < focusIndex)
108 return true;
109
110 return false;
111 }
112
113
114 /**
115 * <p>
116 * Determines readOnly for a "plus one" process.
117 * ReadOnly will be true for any step past the next available step
118 * </p>
119 * @param model the model instance to use.
120 * When the model is passed in a call to model.getPath should return the
121 * path for the "current" node.
122 * @param defaultReturnValue if the current and focus nodes aren't
123 * siblings in the tree, this value will be returned.
124 * @return whether or not the current node should be immediate.
125 */
126 public static boolean isReadOnly(
127 MenuModel model,
128 boolean defaultReturnValue
129 )
130 {
131 Object focusPath = model.getFocusRowKey();
132 Object currPath = model.getRowKey();
133 boolean returnDefault = _hasDifferentAncestors(model,
134 focusPath,
135 currPath);
136
137 if (returnDefault)
138 {
139 model.setRowKey(currPath);
140 return defaultReturnValue;
141 }
142
143
144 if (focusPath.equals(currPath))
145 {
146 model.setRowKey(currPath);
147 return true;
148 }
149
150 model.setRowKey(focusPath);
151 int focusIndex = model.getRowIndex();
152 model.setRowKey(currPath);
153 int currIndex = model.getRowIndex();
154
155 if (currIndex > (focusIndex + 1))
156 return true;
157
158 return false;
159 }
160
161 private static boolean _hasDifferentAncestors(
162 MenuModel model,
163 Object key1,
164 Object key2
165 )
166 {
167 // if either key is null return true
168 if ( key1 == null || key2 == null)
169 return true;
170
171 int depth = model.getDepth(key2);
172 if ( depth != model.getDepth(key1))
173 {
174 return true;
175 }
176 if (depth > 0)
177 {
178
179 model.setRowKey(key1);
180 Object key1ParentPath = model.getContainerRowKey();
181
182 model.setRowKey(key2);
183 Object key2ParentPath = model.getContainerRowKey();
184
185 if (!key2ParentPath.equals(key1ParentPath))
186 {
187 return true;
188 }
189 }
190
191 return false;
192 }
193
194
195
196 //**********************************
197 // Max process methods
198 //**********************************
199 /**
200 * <p>
201 * Determines immediate for a "max visited" process.
202 * When the current step and the max step are the same
203 * immediate will be true for any previous step, and false otherwise.
204 * If the current step is before the max step,
205 * immediate will be false.
206 * </p>
207 * @param model the model instance to use.
208 * When the model is passed in a call to model.getRowKey should return the
209 * rowKey for the "current" node.
210 * @param maxVisitedRowKey the rowKey to use to determine the max node
211 * @param defaultReturnValue if the current, max, and focus nodes aren't
212 * siblings in the tree, this value will be returned.
213 * @return whether or not the current node should be immediate.
214 */
215 public static boolean isImmediate(
216 MenuModel model,
217 Object maxVisitedRowKey,
218 boolean defaultReturnValue
219 )
220 {
221 Object focusPath = model.getFocusRowKey();
222 Object currPath = model.getRowKey();
223
224 boolean returnDefault = _hasDifferentAncestors(model,
225 focusPath,
226 currPath,
227 maxVisitedRowKey);
228
229 if (returnDefault)
230 {
231 model.setRowKey(currPath);
232 return defaultReturnValue;
233 }
234
235
236 model.setRowKey(focusPath);
237 int focusIndex = model.getRowIndex();
238 model.setRowKey(maxVisitedRowKey);
239 int maxIndex = model.getRowIndex();
240 model.setRowKey(currPath);
241 int currIndex = model.getRowIndex();
242
243 if (maxIndex == focusIndex &&
244 currIndex < focusIndex)
245 return true;
246
247 return false;
248 }
249
250 /**
251 * <p>
252 * Determines readOnly for a "max visited" process.
253 * When the current step and the max step are the same,
254 * readOnly will be true for any step past the next available step.
255 * If the current step is before the max step,
256 * then readOnly will be true for any step past the max step.
257 * </p>
258 * @param model the model instance to use.
259 * When the model is passed in a call to model.getRowKey should return the
260 * rowKey for the "current" node.
261 * @param maxVisitedRowKey the rowKey to use to determine the max node
262 * @param defaultReturnValue if the current, max, and focus nodes aren't
263 * siblings in the tree, this value will be returned.
264 * @return whether or not the current node should be immediate.
265 */
266 public static boolean isReadOnly(
267 MenuModel model,
268 Object maxVisitedRowKey,
269 boolean defaultReturnValue
270 )
271 {
272 Object focusPath = model.getFocusRowKey();
273 Object currPath = model.getRowKey();
274
275 boolean returnDefault = _hasDifferentAncestors(model,
276 focusPath,
277 currPath,
278 maxVisitedRowKey);
279
280 if (returnDefault)
281 {
282 model.setRowKey(currPath);
283 return defaultReturnValue;
284 }
285
286 if (focusPath.equals(currPath))
287 {
288 model.setRowKey(currPath);
289 return true;
290 }
291
292 model.setRowKey(focusPath);
293 int focusIndex = model.getRowIndex();
294
295 model.setRowKey(maxVisitedRowKey);
296 int maxIndex = model.getRowIndex();
297
298 if (maxIndex == focusIndex)
299 maxIndex = maxIndex + 1;
300
301 model.setRowKey(currPath);
302 int currIndex = model.getRowIndex();
303
304
305 if (currIndex > maxIndex)
306 return true;
307
308 return false;
309 }
310
311
312 // Plus One case
313
314 /**
315 * For the Plus One case, a stop is considered visited if,
316 * - it's before the current or,
317 * - is the current stop itself
318 *
319 * @param model the menuModel instance to use. When the model is passed in,
320 * a call to model.getRowKey should return the rowKey for the "current" node.
321 * @param defaultReturnValue if the current and focus nodes aren't
322 * siblings in the tree, this value will be returned.
323 * @return whether or not the current node has been visited.
324 */
325 public static boolean isVisited(
326 MenuModel model,
327 boolean defaultReturnValue
328 )
329 {
330 Object focusPath = model.getFocusRowKey();
331 Object currPath = model.getRowKey();
332
333 boolean returnDefault = _hasDifferentAncestors (model,
334 focusPath,
335 currPath);
336
337 if (returnDefault)
338 {
339 model.setRowKey(currPath);
340 return defaultReturnValue;
341 }
342
343 // current node is active
344 if (focusPath.equals(currPath))
345 {
346 model.setRowKey(currPath);
347 return true;
348 }
349
350 // nodes before the current node are visited
351 model.setRowKey(focusPath);
352 int focusIndex = model.getRowIndex();
353 model.setRowKey(currPath);
354 int currIndex = model.getRowIndex();
355
356 if (currIndex <= focusIndex)
357 return true;
358
359 return false;
360 }
361
362 /**
363 * For the Max Visited case, a stop is considered visited if
364 * - a stop is before the active stop
365 * - or is the active stop
366 *
367 * @param model the menuModel instance to use. When the model is passed in, a
368 * call to model.getRowKey should return the rowKey for the "current" node.
369 * @param maxVisitedRowKey the rowKey to use to determine the max visited node
370 * @param defaultReturnValue if the current, maxVisited and focus nodes aren't
371 * siblings in the tree, this value will be returned.
372 * @return whether or not the current node has been visited.
373 */
374 public static boolean isVisited (
375 MenuModel model,
376 Object maxVisitedRowKey,
377 boolean defaultReturnValue)
378 {
379 Object focusPath = model.getFocusRowKey();
380 Object currPath = model.getRowKey();
381
382 boolean returnDefault = _hasDifferentAncestors(model,
383 focusPath,
384 currPath,
385 maxVisitedRowKey);
386
387 if (returnDefault)
388 {
389 model.setRowKey(currPath);
390 return defaultReturnValue;
391 }
392
393 // on active node
394 if (focusPath.equals(currPath))
395 {
396 model.setRowKey(currPath);
397 return true;
398 }
399
400 model.setRowKey(maxVisitedRowKey);
401 int maxIndex = model.getRowIndex();
402
403 model.setRowKey(currPath);
404 int currIndex = model.getRowIndex();
405
406 if (currIndex <= maxIndex)
407 return true;
408
409 return false;
410 }
411
412 /**
413 * Get the rowKey of the max visited node in the respective process.
414 * If null set maxVisitedRowKey to be the focus rowKey.
415 * If set but the focus rowKey is after maxVisitedRowKey,
416 * set maxVisitedRowKey to the focus rowKey.
417 *
418 * @param model the model instance to use.
419 * When the model is passed in a call to model.getRowKey should return the
420 * key for the "current" node.
421 * @param maxPathKey this key is used to store the maxVisitedRowKey in both the
422 * session and request map.
423 * @return the rowKey to the "max visited" node.
424 */
425 @SuppressWarnings("unchecked")
426 public static Object getMaxVisitedRowKey(
427 MenuModel model,
428 String maxPathKey
429 )
430 {
431 assert(maxPathKey != null);
432
433 ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
434 Map<String, Object> requestMap = externalContext.getRequestMap();
435
436 Object maxRowKey = requestMap.get(maxPathKey);
437 boolean mustCompareWithFocusRow = true;
438
439 //TODO - if I change this to use pageFlowScope it doesn't work.
440 // figure out why.
441 Map<String,Object> sessionMap = externalContext.getSessionMap();
442 Map<Object,Object> maxPathMap = (Map<Object,Object>)sessionMap.get(maxPathKey);
443 if (maxPathMap == null)
444 maxPathMap = new HashMap<Object,Object>();
445
446 Object focusRowKey = model.getFocusRowKey();
447 Object parentRowKey = model.getContainerRowKey(focusRowKey);
448
449 if (parentRowKey == null)
450 parentRowKey = _MAX_PATH_TOP;
451
452 if (maxRowKey == null)
453 {
454 maxRowKey = maxPathMap.get(parentRowKey);
455 if (maxRowKey == null)
456 {
457 maxRowKey = focusRowKey;
458 maxPathMap.put(parentRowKey, maxRowKey);
459 sessionMap.put(maxPathKey, maxPathMap);
460
461 // for fast access store maxRowKey in the requestMap
462 requestMap.put(maxPathKey, maxRowKey);
463 mustCompareWithFocusRow = false;
464 }
465 }
466
467 if (mustCompareWithFocusRow)
468 {
469 // figure out if focusRowKey > maxPath, if so set a new maxPath,
470 // the result is set on the request so we do this once per request.
471 Object currPath = model.getRowKey();
472 boolean hasDifferentAncestors = _hasDifferentAncestors(model,
473 focusRowKey,
474 maxRowKey);
475
476 //TODO - should I default this to focusPath?
477 if (hasDifferentAncestors)
478 {
479 model.setRowKey(currPath);
480 return null;
481 }
482
483 model.setRowKey(focusRowKey);
484 int focusRowIndex = model.getRowIndex();
485 model.setRowKey(maxRowKey);
486 int maxRowIndex = model.getRowIndex();
487
488 if (focusRowIndex > maxRowIndex)
489 {
490 maxPathMap.put(parentRowKey, focusRowKey);
491 sessionMap.put(maxPathKey, maxPathMap);
492 requestMap.put(maxPathKey, focusRowKey);
493 }
494 else
495 {
496 requestMap.put(maxPathKey, maxRowKey);
497 }
498
499 model.setRowKey(currPath);
500 }
501
502 return maxRowKey;
503 }
504
505 @SuppressWarnings("unchecked")
506 public static void clearMaxPath(
507 String maxPathKey
508 )
509 {
510 if (maxPathKey != null)
511 {
512 Map<String, Object> sessionMap = FacesContext.getCurrentInstance().getExternalContext().getSessionMap();
513 sessionMap.put(maxPathKey, null);
514 }
515 }
516
517
518 private static boolean _hasDifferentAncestors(
519 MenuModel model,
520 Object key1,
521 Object key2,
522 Object key3
523 )
524 {
525 boolean diff1 = _hasDifferentAncestors(model, key1, key2);
526
527 if (diff1)
528 return true;
529
530 boolean diff2 = _hasDifferentAncestors(model, key1, key3);
531
532 if ( diff2)
533 return true;
534
535 return false;
536
537 }
538
539
540
541 private static final Object _MAX_PATH_TOP = new Object();
542
543 }
544