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