001/**
002 * Copyright 2005-2018 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.uif.lifecycle;
017
018import java.util.ArrayList;
019import java.util.HashSet;
020import java.util.LinkedHashSet;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Map;
024import java.util.Queue;
025
026import org.apache.commons.lang.StringUtils;
027import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
028import org.kuali.rice.krad.uif.UifConstants;
029import org.kuali.rice.krad.uif.component.Component;
030import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle.LifecycleEvent;
031import org.kuali.rice.krad.uif.util.CopyUtils;
032import org.kuali.rice.krad.uif.util.LifecycleElement;
033import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
034import org.kuali.rice.krad.uif.util.ProcessLogger;
035import org.kuali.rice.krad.uif.util.RecycleUtils;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039/**
040 * Base abstract implementation for a lifecycle phase.
041 *
042 * @author Kuali Rice Team (rice.collab@kuali.org)
043 */
044public abstract class ViewLifecyclePhaseBase implements ViewLifecyclePhase {
045    private final Logger LOG = LoggerFactory.getLogger(ViewLifecyclePhaseBase.class);
046
047    private LifecycleElement element;
048    private Component parent;
049    private String viewPath;
050    private String path;
051    private int depth;
052
053    private List<String> refreshPaths;
054
055    private ViewLifecyclePhase predecessor;
056    private ViewLifecyclePhase nextPhase;
057
058    private boolean processed;
059    private boolean completed;
060
061    private HashSet<String> pendingSuccessors = new LinkedHashSet<String>();
062
063    private ViewLifecycleTask<?> currentTask;
064
065    private List<ViewLifecycleTask<?>> tasks;
066    private List<ViewLifecycleTask<?>> skipLifecycleTasks;
067
068    /**
069     * Resets this phase for recycling.
070     */
071    public void recycle() {
072        trace("recycle");
073
074        element = null;
075        path = null;
076        viewPath = null;
077        depth = 0;
078        predecessor = null;
079        nextPhase = null;
080        processed = false;
081        completed = false;
082        refreshPaths = null;
083        pendingSuccessors = new LinkedHashSet<String>();
084    }
085
086    /**
087     * {@inheritDoc}
088     */
089    @Override
090    public void prepare(LifecycleElement element, Component parent, String parentPath, List<String> refreshPaths) {
091        this.path = parentPath;
092
093        String parentViewPath = parent == null ? null : parent.getViewPath();
094        if (StringUtils.isEmpty(parentViewPath)) {
095            this.viewPath = path;
096        } else {
097            this.viewPath = parentViewPath + '.' + path;
098        }
099
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}