View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.lifecycle;
17  
18  import java.util.ArrayList;
19  import java.util.LinkedHashSet;
20  import java.util.LinkedList;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.Queue;
24  import java.util.Set;
25  
26  import org.apache.commons.lang.StringUtils;
27  import org.kuali.rice.krad.uif.UifConstants;
28  import org.kuali.rice.krad.uif.component.Component;
29  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle.LifecycleEvent;
30  import org.kuali.rice.krad.uif.lifecycle.initialize.AssignIdsTask;
31  import org.kuali.rice.krad.uif.util.ComponentUtils;
32  import org.kuali.rice.krad.uif.util.CopyUtils;
33  import org.kuali.rice.krad.uif.util.LifecycleElement;
34  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
35  import org.kuali.rice.krad.uif.util.ProcessLogger;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  /**
40   * Base abstract implementation for a lifecycle phase.
41   *
42   * @author Kuali Rice Team (rice.collab@kuali.org)
43   */
44  public abstract class ViewLifecyclePhaseBase implements ViewLifecyclePhase {
45      private final Logger LOG = LoggerFactory.getLogger(ViewLifecyclePhaseBase.class);
46  
47      private LifecycleElement element;
48      private Object model;
49      private Component parent;
50      private String viewPath;
51      private String path;
52      private int depth;
53  
54      private List<String> refreshPaths;
55  
56      private ViewLifecyclePhaseBase predecessor;
57      private ViewLifecyclePhaseBase nextPhase;
58  
59      private boolean processed;
60      private boolean completed;
61  
62      private Set<String> pendingSuccessors = new LinkedHashSet<String>();
63  
64      private ViewLifecycleTask<?> currentTask;
65  
66      /**
67       * Resets this phase for recycling.
68       */
69      protected void recycle() {
70          trace("recycle");
71          element = null;
72          model = null;
73          path = null;
74          viewPath = null;
75          depth = 0;
76          predecessor = null;
77          nextPhase = null;
78          processed = false;
79          completed = false;
80          refreshPaths = null;
81          pendingSuccessors.clear();
82      }
83  
84      /**
85       * Prepares this phase for reuse.
86       *
87       * @param element The element to be processed by this phase
88       * @param model The model associated with the lifecycle at this phase
89       * @param path Path to the component relative to the active view
90       * @param refreshPaths list of paths to run lifecycle on when executing a refresh lifecycle
91       * @param parent The parent element. For top-down phases, this component will be associated
92       * with the predecessor phase. For bottom-up phases (rendering), this element will be
93       * associated with a successor phases
94       * @param nextPhase The lifecycle phase to queue directly upon completion of this phase, if
95       * applicable
96       * @see LifecyclePhaseFactory
97       */
98      protected void prepare(LifecycleElement element, Object model, String path, List<String> refreshPaths,
99              Component parent, ViewLifecyclePhaseBase nextPhase) {
100         if (element.getViewStatus().equals(getEndViewStatus())) {
101             LOG.warn(
102                     "Component is already in the expected end status " + getEndViewStatus() + " before this phase " +
103                             element.getClass() + " " + element.getId());
104         }
105 
106         this.model = model;
107         this.path = path;
108 
109         String parentViewPath = parent == null ? null : parent.getViewPath();
110         if (StringUtils.isEmpty(parentViewPath)) {
111             this.viewPath = path;
112         } else {
113             this.viewPath = parentViewPath + "." + path;
114         }
115 
116         this.element = CopyUtils.unwrap(element);
117         this.refreshPaths = refreshPaths;
118         this.parent = parent;
119         this.nextPhase = nextPhase;
120 
121         trace("prepare");
122     }
123 
124     /**
125      * Executes the lifecycle phase.
126      *
127      * <p>
128      * This method performs state validation and updates component view status. Use
129      * {@link #initializePendingTasks(Queue)} to provide phase-specific behavior.
130      * </p>
131      *
132      * @see java.lang.Runnable#run()
133      */
134     @Override
135     public final void run() {
136         try {
137             ViewLifecycleProcessorBase processor = (ViewLifecycleProcessorBase) ViewLifecycle.getProcessor();
138 
139             validateBeforeProcessing();
140 
141             boolean skipLifecycle = shouldSkipLifecycle();
142 
143             String ntracePrefix = null;
144             String ntraceSuffix = null;
145             try {
146                 if (ViewLifecycle.isTrace() && ProcessLogger.isTraceActive()) {
147                     ntracePrefix = "lc-" + getStartViewStatus() + "-" + getEndViewStatus() + ":";
148                     ntraceSuffix = ":" + getElement().getClass().getSimpleName() + (getElement().isRender()?":render":":no-render");
149 
150                     ProcessLogger.ntrace(ntracePrefix, ntraceSuffix, 1000);
151                     ProcessLogger.countBegin(ntracePrefix + ntraceSuffix);
152                 }
153 
154                 String viewStatus = element.getViewStatus();
155                 if (viewStatus != null &&
156                         !viewStatus.equals(getStartViewStatus())) {
157                     trace("dup " + getStartViewStatus() + " " + getEndViewStatus() + " " + viewStatus);
158                 }
159 
160                 processor.setActivePhase(this);
161 
162                 trace("path-update " + element.getViewPath());
163                 
164                 element.setViewPath(getViewPath());
165                 element.getPhasePathMapping().put(getViewPhase(), getViewPath());
166 
167                 Queue<ViewLifecycleTask<?>> pendingTasks = new LinkedList<ViewLifecycleTask<?>>();
168                 if (skipLifecycle) {
169                     initializeSkipLifecyclePendingTasks(pendingTasks);
170                 } else {
171                     initializePendingTasks(pendingTasks);
172                 }
173 
174                 while (!pendingTasks.isEmpty()) {
175                     ViewLifecycleTask<?> task = pendingTasks.poll();
176 
177                     currentTask = task;
178                     task.run();
179                     currentTask = null;
180                 }
181 
182                 element.setViewStatus(getEndViewStatus());
183                 processed = true;
184 
185             } finally {
186                 processor.setActivePhase(null);
187 
188                 if (ViewLifecycle.isTrace() && ProcessLogger.isTraceActive()) {
189                     ProcessLogger.countEnd(ntracePrefix + ntraceSuffix, 
190                             getElement().getClass() + " " + getElement().getId());
191                 }
192             }
193 
194             if (skipLifecycle) {
195                 notifyCompleted();
196             } else {
197                 assert pendingSuccessors.isEmpty() : pendingSuccessors;
198 
199                 Queue<ViewLifecyclePhase> successors = new LinkedList<ViewLifecyclePhase>();
200 
201                 initializeSuccessors(successors);
202                 processSuccessors(successors);
203             }
204         } catch (Throwable t) {
205             trace("error");
206             LOG.warn("Error in lifecycle phase " + this, t);
207 
208             if (t instanceof RuntimeException) {
209                 throw (RuntimeException) t;
210             } else if (t instanceof Error) {
211                 throw (Error) t;
212             } else {
213                 throw new IllegalStateException("Unexpected error in lifecycle phase " + this, t);
214             }
215         }
216     }
217 
218     /**
219      * Indicates whether the lifecycle should be skipped for the current component.
220      *
221      * <p>Elements are always processed in the pre process phase, or in the case of the element or one
222      * of its childs being refreshed. If these conditions are false, the element method
223      * {@link org.kuali.rice.krad.uif.util.LifecycleElement#skipLifecycle()} is invoked to determine if
224      * the lifecycle can be skipped.</p>
225      *
226      * @return boolean true if the lifecycle should be skipped, false if not
227      * @see org.kuali.rice.krad.uif.util.LifecycleElement#skipLifecycle()
228      */
229     protected boolean shouldSkipLifecycle() {
230         // we always want to run the preprocess phase so ids are assigned
231         boolean isPreProcessPhase = getViewPhase().equals(UifConstants.ViewPhases.PRE_PROCESS);
232 
233         // if the component is being refreshed its lifecycle should not be skipped
234         boolean isRefreshComponent = ViewLifecycle.isRefreshComponent(getViewPhase(), getViewPath());
235 
236         // if a child of this component is being refresh its lifecycle should not be skipped
237         boolean includesRefreshComponent = false;
238         if (StringUtils.isNotBlank(ViewLifecycle.getRefreshComponentPhasePath(getViewPhase()))) {
239             includesRefreshComponent = ViewLifecycle.getRefreshComponentPhasePath(getViewPhase()).startsWith(getViewPath());
240         }
241 
242         boolean skipLifecycle = false;
243         if (!(isPreProcessPhase || isRefreshComponent || includesRefreshComponent)) {
244             // delegate to the component to determine whether skipping lifecycle is ok
245             skipLifecycle = element.skipLifecycle();
246         }
247 
248         return skipLifecycle;
249     }
250 
251     /**
252      * Validates this phase and thread state before processing and logs activity.
253      *
254      * @see #run()
255      */
256     protected void validateBeforeProcessing() {
257         if (processed) {
258             throw new IllegalStateException("Lifecycle phase has already been processed " + this);
259         }
260 
261         if (predecessor != null && !predecessor.isProcessed()) {
262             throw new IllegalStateException("Predecessor phase has not completely processed " + this);
263         }
264 
265         if (!ViewLifecycle.isActive()) {
266             throw new IllegalStateException("No view lifecyle is not active on the current thread");
267         }
268 
269         if (LOG.isDebugEnabled()) {
270             trace("ready " + getStartViewStatus() + " -> " + getEndViewStatus());
271         }
272     }
273 
274     /**
275      * Adds phases added as successors to the processor, or if there are no pending successors invokes
276      * the complete notification step.
277      *
278      * @param successors phases to process
279      */
280     protected void processSuccessors(Queue<ViewLifecyclePhase> successors) {
281         for (ViewLifecyclePhase successor : successors) {
282             if (!pendingSuccessors.add(successor.getParentPath())) {
283                 ViewLifecycle.reportIllegalState("Already pending " + successor + "\n" + this);
284             }
285         }
286 
287         trace("processed " + pendingSuccessors);
288 
289         if (pendingSuccessors.isEmpty()) {
290             notifyCompleted();
291         } else {
292             for (ViewLifecyclePhase successor : successors) {
293                 if (successor instanceof ViewLifecyclePhaseBase) {
294                     ViewLifecyclePhaseBase successorBase = (ViewLifecyclePhaseBase) successor;
295                     assert successorBase.predecessor == null : this + " " + successorBase;
296 
297                     successorBase.predecessor = this;
298                     successorBase.depth = this.depth + 1;
299                     successorBase.trace("succ-pend");
300                 }
301 
302                 ViewLifecycle.getProcessor().offerPendingPhase(successor);
303             }
304         }
305     }
306 
307     /**
308      * Initializes queue of pending tasks phases that need to be executed when skipping the full lifecycle.
309      *
310      * @param tasks The queue of tasks to perform
311      */
312     protected void initializeSkipLifecyclePendingTasks(Queue<ViewLifecycleTask<?>> tasks) {
313 
314     }
315 
316     /**
317      * Initializes queue of pending tasks phases.
318      *
319      * <p>This method will be called before during processing to determine which tasks to perform at
320      * this phase.</p>
321      *
322      * @param tasks The queue of tasks to perform.
323      */
324     protected abstract void initializePendingTasks(Queue<ViewLifecycleTask<?>> tasks);
325 
326     /**
327      * Initializes queue of successor phases.
328      *
329      * <p>This method will be called while processing this phase after all tasks have been performed,
330      * to determine phases to queue for successor processing. This phase will not be considered
331      * complete until all successors queued by this method, and all subsequent successor phases,
332      * have completed processing.</p>
333      *
334      * @param successors The queue of successor phases
335      */
336     protected void initializeSuccessors(Queue<ViewLifecyclePhase> successors) {
337         if (ViewLifecycle.isRefreshLifecycle() && (refreshPaths != null)) {
338             String currentPath = getViewPath();
339 
340             boolean withinRefreshComponent = currentPath.startsWith(ViewLifecycle.getRefreshComponentPhasePath(getViewPhase()));
341             if (withinRefreshComponent) {
342                 initializeAllLifecycleSuccessors(successors);
343             } else if (refreshPaths.contains(currentPath) || StringUtils.isBlank(currentPath)) {
344                 initializeRefreshPathSuccessors(successors);
345             }
346 
347             return;
348         }
349 
350         initializeAllLifecycleSuccessors(successors);
351     }
352 
353     /**
354      * Initializes only the lifecycle successors referenced by paths within {@link #getRefreshPaths()}.
355      *
356      * @param successors the successor queue
357      */
358     protected void initializeRefreshPathSuccessors(Queue<ViewLifecyclePhase> successors) {
359         LifecycleElement element = getElement();
360 
361         String nestedPathPrefix;
362         Component nestedParent;
363         if (element instanceof Component) {
364             nestedParent = (Component) element;
365             nestedPathPrefix = "";
366         } else {
367             nestedParent = getParent();
368             nestedPathPrefix = getParentPath() + ".";
369         }
370 
371         List<String> nestedProperties = getNestedPropertiesForRefreshPath();
372 
373         for (String nestedProperty : nestedProperties) {
374             String nestedPath = nestedPathPrefix + nestedProperty;
375 
376             LifecycleElement nestedElement = ObjectPropertyUtils.getPropertyValue(element, nestedProperty);
377             if (nestedElement != null) {
378                 ViewLifecyclePhase nestedPhase = initializeSuccessor(nestedElement, nestedPath, nestedParent);
379                 successors.add(nestedPhase);
380             }
381         }
382     }
383 
384     /**
385      * Determines the list of child properties for the current phase component that are in the refresh
386      * paths and should be processed next.
387      *
388      * @return list of property names relative to the component the phase is currently processing
389      */
390     protected List<String> getNestedPropertiesForRefreshPath() {
391         List<String> nestedProperties = new ArrayList<String>();
392 
393         String currentPath = getViewPath();
394         if (currentPath == null) {
395             currentPath = "";
396         }
397 
398         if (StringUtils.isNotBlank(currentPath)) {
399             currentPath += ".";
400         }
401 
402         // to get the list of children, the refresh path must start with the path of the component being
403         // processed. If the child path is nested, we get the top most property first
404         for (String refreshPath : refreshPaths) {
405             if (!refreshPath.startsWith(currentPath)) {
406                 continue;
407             }
408 
409             String nestedProperty = StringUtils.substringAfter(refreshPath, currentPath);
410 
411             if (StringUtils.isBlank(nestedProperty)) {
412                 continue;
413             }
414 
415             if (StringUtils.contains(nestedProperty, ".")) {
416                 nestedProperty = StringUtils.substringBefore(nestedProperty, ".");
417             }
418 
419             if (!nestedProperties.contains(nestedProperty)) {
420                 nestedProperties.add(nestedProperty);
421             }
422         }
423 
424         return nestedProperties;
425     }
426 
427     /**
428      * Initializes all lifecycle phase successors.
429      *
430      * @param successors The successor queue.
431      */
432     protected void initializeAllLifecycleSuccessors(Queue<ViewLifecyclePhase> successors) {
433         LifecycleElement element = getElement();
434 
435         String nestedPathPrefix;
436         Component nestedParent;
437         if (element instanceof Component) {
438             nestedParent = (Component) element;
439             nestedPathPrefix = "";
440         } else {
441             nestedParent = getParent();
442             nestedPathPrefix = getParentPath() + ".";
443         }
444 
445         for (Map.Entry<String, LifecycleElement> nestedElementEntry : ViewLifecycleUtils.getElementsForLifecycle(
446                 element, getViewPhase()).entrySet()) {
447             String nestedPath = nestedPathPrefix + nestedElementEntry.getKey();
448             LifecycleElement nestedElement = nestedElementEntry.getValue();
449 
450             if (nestedElement != null && !getEndViewStatus().equals(nestedElement.getViewStatus())) {
451                 ViewLifecyclePhase nestedPhase = initializeSuccessor(nestedElement, nestedPath, nestedParent);
452                 successors.offer(nestedPhase);
453             }
454         }
455     }
456 
457     /**
458      * Initializes a successor of this phase for a given nested element.
459      *
460      * @param nestedElement The lifecycle element.
461      * @param nestedPath The path, relative to the parent element.
462      * @param nestedParent The parent component of the nested element.
463      * @return successor phase
464      */
465     protected abstract ViewLifecyclePhase initializeSuccessor(LifecycleElement nestedElement, String nestedPath,
466             Component nestedParent);
467 
468     /**
469      * May be overridden in order to check for illegal state based on more concrete assumptions than
470      * can be made here.
471      * 
472      * @throws IllegalStateException If the conditions for completing the lifecycle phase have not been met.
473      */
474     protected void verifyCompleted() {
475     }
476 
477     /**
478      * Notifies predecessors that this task has completed.
479      */
480     protected final void notifyCompleted() {
481         trace("complete");
482 
483         completed = true;
484 
485         LifecycleEvent event = getEventToNotify();
486         if (event != null) {
487             ViewLifecycle.getActiveLifecycle().invokeEventListeners(event, ViewLifecycle.getView(),
488                     ViewLifecycle.getModel(), element);
489         }
490 
491         element.notifyCompleted(this);
492 
493         if (nextPhase != null) {
494             assert nextPhase.predecessor == null : this + " " + nextPhase;
495 
496             // Assign a predecessor to the next phase, to defer notification until
497             // after all phases in the chain have completed processing.
498             if (predecessor != null) {
499                 // Common case: "catch up" phase automatically spawned to bring
500                 // a component up to the right status before phase processing.
501                 // Swap the next phase in for this phase in the graph.
502                 nextPhase.predecessor = predecessor;
503                 nextPhase.depth = predecessor.depth + 1;
504             } else {
505                 // Initial phase chain:  treat the next phase as a successor so that
506                 // this phase (and therefore the controlling thread) will be notified
507                 nextPhase.predecessor = this;
508                 nextPhase.depth = this.depth + 1;
509                 synchronized (pendingSuccessors) {
510                     pendingSuccessors.add(nextPhase.getParentPath());
511                 }
512             }
513 
514             ViewLifecycle.getProcessor().pushPendingPhase(nextPhase);
515             return;
516         }
517 
518         synchronized (this) {
519             if (predecessor != null) {
520                 synchronized (predecessor) {
521                     predecessor.pendingSuccessors.remove(getParentPath());
522                     if (predecessor.pendingSuccessors.isEmpty()) {
523                         predecessor.notifyCompleted();
524                     }
525                     LifecyclePhaseFactory.recycle(this);
526                 }
527             } else {
528                 trace("notify");
529                 notifyAll();
530             }
531         }
532     }
533 
534     /**
535      * {@inheritDoc}
536      */
537     @Override
538     public final LifecycleElement getElement() {
539         return element;
540     }
541 
542     /**
543      * {@inheritDoc}
544      */
545     @Override
546     public final Object getModel() {
547         return model;
548     }
549 
550     /**
551      * {@inheritDoc}
552      */
553     @Override
554     public final Component getParent() {
555         return this.parent;
556     }
557 
558     /**
559      * {@inheritDoc}
560      */
561     @Override
562     public String getParentPath() {
563         return this.path;
564     }
565 
566     /**
567      * {@inheritDoc}
568      */
569     @Override
570     public List<String> getRefreshPaths() {
571         return refreshPaths;
572     }
573 
574     /**
575      * {@inheritDoc}
576      */
577     @Override
578     public String getViewPath() {
579         return this.viewPath;
580     }
581 
582     /**
583      * @param viewPath the viewPath to set
584      */
585     public void setViewPath(String viewPath) {
586         this.viewPath = viewPath;
587     }
588 
589     /**
590      * {@inheritDoc}
591      */
592     @Override
593     public int getDepth() {
594         return this.depth;
595     }
596 
597     /**
598      * {@inheritDoc}
599      */
600     @Override
601     public final boolean isProcessed() {
602         return processed;
603     }
604 
605     /**
606      * {@inheritDoc}
607      */
608     @Override
609     public final boolean isComplete() {
610         return completed;
611     }
612 
613     /**
614      * {@inheritDoc}
615      */
616     @Override
617     public ViewLifecyclePhase getPredecessor() {
618         return predecessor;
619     }
620 
621     /**
622      * {@inheritDoc}
623      */
624     @Override
625     public ViewLifecycleTask<?> getCurrentTask() {
626         return this.currentTask;
627     }
628 
629     /**
630      * {@inheritDoc}
631      */
632     @Override
633     public String toString() {
634         StringBuilder sb = new StringBuilder();
635         Queue<ViewLifecyclePhase> toPrint = new LinkedList<ViewLifecyclePhase>();
636         toPrint.offer(this);
637         while (!toPrint.isEmpty()) {
638             ViewLifecyclePhase tp = toPrint.poll();
639 
640             if (tp.getElement() == null) {
641                 sb.append("\n      ");
642                 sb.append(tp.getClass().getSimpleName());
643                 sb.append(" (recycled)");
644                 continue;
645             }
646 
647             String indent;
648             if (tp == this) {
649                 if (this.model != null) {
650                     sb.append("Model: ");
651                     sb.append(this.model.getClass().getSimpleName());
652                 }
653                 sb.append("\nProcessed? ");
654                 sb.append(processed);
655                 indent = "\n";
656             } else {
657                 indent = "\n    ";
658             }
659             sb.append(indent);
660 
661             sb.append(tp.getClass().getSimpleName());
662             sb.append(" ");
663             sb.append(System.identityHashCode(tp));
664             sb.append(" ");
665             sb.append(tp.getViewPath());
666             sb.append(" ");
667             sb.append(tp.getElement().getClass().getSimpleName());
668             sb.append(" ");
669             sb.append(tp.getElement().getId());
670             sb.append(" ");
671             sb.append(pendingSuccessors);
672 
673             if (tp == this) {
674                 sb.append("\nPredecessor Phases:");
675             }
676 
677             ViewLifecyclePhase tpredecessor = tp.getPredecessor();
678             if (tpredecessor != null) {
679                 toPrint.add(tpredecessor);
680             }
681         }
682         return sb.toString();
683     }
684 
685     /**
686      * Logs a trace message related to processing this lifecycle, when tracing is active and
687      * debugging is enabled.
688      *
689      * @param step The step in processing the phase that has been reached.
690      * @see ViewLifecycle#isTrace()
691      */
692     protected void trace(String step) {
693         if (ViewLifecycle.isTrace() && LOG.isDebugEnabled()) {
694             String msg = System.identityHashCode(this) + " " + getClass() + " " + step + " " + path + " " +
695                     (element == null ? "(recycled)" : element.getClass() + " " + element.getId());
696             LOG.debug(msg);
697         }
698     }
699 
700 }