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}