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