001 package edu.nrao.sss.model.project; 002 003 import java.io.FileNotFoundException; 004 import java.io.Reader; 005 import java.io.Writer; 006 import java.util.Date; 007 import java.util.UUID; 008 009 import javax.xml.bind.JAXBException; 010 import javax.xml.bind.annotation.XmlAttribute; 011 import javax.xml.bind.annotation.XmlElement; 012 import javax.xml.bind.annotation.XmlID; 013 import javax.xml.bind.annotation.XmlRootElement; 014 import javax.xml.bind.annotation.XmlTransient; 015 import javax.xml.stream.XMLStreamException; 016 017 import static edu.nrao.sss.util.EventStatus.*; 018 019 import edu.nrao.sss.util.EventStatus; 020 import edu.nrao.sss.util.Identifiable; 021 import edu.nrao.sss.util.IllegalTransitionException; 022 import edu.nrao.sss.util.JaxbUtility; 023 024 /** 025 * A single execution of a {@link SchedulingBlock}. A scheduling block 026 * is a template for what should happen; an execution block is a record 027 * of what actually happened. 028 * <p> 029 * <b>Version Info:</b> 030 * <table style="margin-left:2em"> 031 * <tr><td>$Revision: 2190 $</td> 032 * <tr><td>$Date: 2009-04-13 15:15:03 -0600 (Mon, 13 Apr 2009) $</td> 033 * <tr><td>$Author: dharland $</td> 034 * </table></p> 035 * 036 * @author David M. Harland 037 * @since 2006-02-24 038 */ 039 @XmlRootElement //primarily for testing 040 public class ExecutionBlock 041 implements Cloneable, Identifiable, ScheduleEntry 042 { 043 private static final String EMPTY_SCRIPT = "# This script is empty."; 044 private static final String NO_COMMENTS = ""; 045 046 //IDENTIFICATION 047 private Long id; //A unique identifier for the persistence layer. 048 049 @XmlAttribute(required=true) 050 @XmlID 051 String xmlId; 052 053 //CONTAINER (SchedulingBlock) 054 private SchedulingBlock schedBlock; 055 056 //OTHER ATTRIBUTES 057 private EventStatus execStatus; 058 private Date execStatusChangeDate; 059 private String comments; 060 private StringBuilder execScript; 061 062 @XmlElement private boolean wasAborted; 063 064 /** 065 * Creates a new instance. 066 */ 067 public ExecutionBlock() 068 { 069 this(null); 070 } 071 072 /** 073 * Creates a new instance that will be part of the given scheduling block. 074 */ 075 ExecutionBlock(SchedulingBlock container) 076 { 077 xmlId = "execBlock-" + UUID.randomUUID().toString(); 078 schedBlock = container; 079 080 initialize(); 081 } 082 083 /** Initializes the instance variables of this class. */ 084 private void initialize() 085 { 086 id = Identifiable.UNIDENTIFIED; 087 execStatus = NOT_READY_TO_BE_SCHEDULED; 088 execStatusChangeDate = new Date(); 089 execScript = new StringBuilder(EMPTY_SCRIPT); 090 wasAborted = false; 091 comments = NO_COMMENTS; 092 } 093 094 //============================================================================ 095 // IDENTIFICATION 096 //============================================================================ 097 098 /* (non-Javadoc) 099 * @see edu.nrao.sss.util.Identifiable#getId() 100 */ 101 @XmlAttribute 102 public Long getId() 103 { 104 return id; 105 } 106 107 @SuppressWarnings("unused") //used by Hibernate & JAXB 108 private void setId(Long id) 109 { 110 this.id = id; 111 } 112 113 /** 114 * Resets this ExecutionBlock's id to UNIDENTIFIED. 115 */ 116 public void clearId() 117 { 118 id = Identifiable.UNIDENTIFIED; 119 } 120 121 //============================================================================ 122 // CONTAINER (SchedulingBlock) 123 //============================================================================ 124 125 /** 126 * Sets this execution block's scheduling block to {@code newSchedBlock} 127 * without contacting either the former or new program block. 128 * This method is used only by the SchedulingBlock class. 129 */ 130 void simplySetSchedulingBlock(SchedulingBlock newSchedBlock) 131 { 132 this.schedBlock = newSchedBlock; 133 } 134 135 /** 136 * Returns the scheduling block to which this execution block belongs. 137 * <p> 138 * This execution block may be one of several that belong to 139 * the same scheduling block. If this execution block belongs to 140 * no scheduling block, the value returned is <i>null</i>.</p> 141 * 142 * @return the scheduling block that contains this execution block. 143 * 144 * @see #hasSchedulingBlock() 145 */ 146 public SchedulingBlock getSchedulingBlock() 147 { 148 return schedBlock; 149 } 150 151 /** 152 * Returns <i>true</i> if this execution block has a non-null scheduling block. 153 * <p> 154 * Execution blocks should normally be contained within, and therefore 155 * have a non-null, scheduling block. However, there are some situations 156 * where this method will return <i>false</i>: 157 * <ol> 158 * <li>This execution block has just been created and its scheduling block 159 * has not yet been set.</li> 160 * <li>A client removed this execution block from its scheduling block and 161 * did not place it in a new scheduling block.</li> 162 * <li>A client explicitly set this execution block's scheduling block to 163 * <i>null</i>.</li> 164 * </ol></p> 165 * 166 * @return 167 * <i>true</i> if this execution block has a non-null scheduling block. 168 * Therefore a return value of <i>true</i> means that you can call 169 * {@link #getSchedulingBlock()} and know that it will return a non-null 170 * object. 171 */ 172 public boolean hasSchedulingBlock() 173 { 174 return schedBlock != null; 175 } 176 177 //============================================================================ 178 // EXECUTION SCRIPT 179 //============================================================================ 180 181 /** 182 * Sets the execution script that represents this execution block. 183 * 184 * @param newScript 185 * a new execution script. This value is copied into this block, not held 186 * by it directly. If this value is <i>null</i> this block will hold a 187 * one-line script, where that one line is a comment. 188 */ 189 @XmlTransient //JAXB will use s/getExecutionScript 190 public void setExecScript(StringBuilder newScript) 191 { 192 execScript.delete(0, execScript.length()); 193 194 if (newScript != null) 195 execScript.append(newScript); 196 else 197 execScript.append(EMPTY_SCRIPT); 198 } 199 200 /** 201 * Returns a copy of the execution script held by this block. 202 * <p> 203 * This method never returns <i>null</i>. If no script has been set, or 204 * if an attempt was made to store a <i>null</i> script, the returned 205 * script will be one line long and that one line will be a comment.</p> 206 * 207 * @return 208 * a copy of the execution script held by this block. 209 */ 210 public StringBuilder getExecScript() 211 { 212 return new StringBuilder().append(execScript); 213 } 214 215 /** 216 * Sets the execution script that represents this execution block. 217 * This is a convenience method that is equivalent to calling 218 * <tt>setExecScript(new StringBuilder(newScript))</tt>, provided 219 * <tt>newScript</tt> is not <i>null</i>. 220 * 221 * @param newScript 222 * a new execution script. 223 * If this value is <i>null</i> this block will hold a 224 * one-line script, where that one line is a comment. 225 */ 226 @XmlElement 227 public void setExecutionScript(String newScript) 228 { 229 setExecScript(newScript == null ? null : new StringBuilder(newScript)); 230 } 231 232 /** 233 * Returns the execution script held by this block. 234 * This is a convenience method that is equivalent to calling 235 * <tt>getExecScript().toString()</tt>. 236 * 237 * @return the execution script held by this block. 238 * 239 * @see #getExecScript() 240 */ 241 public String getExecutionScript() 242 { 243 return execScript.toString(); 244 } 245 246 //============================================================================ 247 // COMMENTS 248 //============================================================================ 249 250 /** 251 * Sets comments about this execution block. 252 * The most common use of comments for execution blocks is expected to occur 253 * when an operator wants to make note of why a block failed to execute. 254 * 255 * @param replacementComments 256 * free-form text about this execution block. 257 * These comments replace all previously set comments. 258 * A <i>null</i> value will be replaced by the empty string (<tt>""</tt>). 259 * 260 * @see #appendComments(String) 261 */ 262 public void setComments(String replacementComments) 263 { 264 comments = (replacementComments == null) ? NO_COMMENTS : replacementComments; 265 } 266 267 /** 268 * Adds additional comments to those already associated with this execution block. 269 * 270 * @param additionalComments 271 * new, additional, comments about this execution block. 272 * 273 * @see #setComments(String) 274 */ 275 public void appendComments(String additionalComments) 276 { 277 if ((additionalComments != null) && (additionalComments.length() > 0)) 278 { 279 if (!comments.equals(NO_COMMENTS)) 280 comments = comments + System.getProperty("line.separator"); 281 282 comments = comments + additionalComments; 283 } 284 } 285 286 /** 287 * Returns comments about this execution block. 288 * The value returned is guaranteed to be non-null. 289 * 290 * @return 291 * free-form text about this execution block. 292 * 293 * @see #getComments() 294 * @see #appendComments(String) 295 * @see #setComments(String) 296 */ 297 public String getComments() 298 { 299 return comments; 300 } 301 302 //============================================================================ 303 // STATUS 304 //============================================================================ 305 306 /** 307 * Returns <i>true</i> if this block is part of a 308 * {@link SchedulingBlock#isTest() test scheduling block}. 309 * @return <i>true</i> if this block is part of a test scheduling block. 310 */ 311 public boolean isTest() 312 { 313 return hasSchedulingBlock() && getSchedulingBlock().isTest(); 314 } 315 316 /** Changes status, if permitted, or throws exception, if not. */ 317 private void changeStatusTo(EventStatus newStatus) 318 throws IllegalTransitionException 319 { 320 if (mayChangeStatusTo(newStatus)) 321 { 322 execStatus = newStatus; 323 execStatusChangeDate = new Date(); 324 } 325 else 326 { 327 String explanation = 328 isTest() ? "May not make this status transition on a TEST block." 329 : ""; 330 331 throw new IllegalTransitionException("executionStatus", 332 getExecutionStatus().toString(), 333 newStatus.toString(), 334 explanation); 335 } 336 } 337 338 /** Returns <i>true</i> if status may be changed to newStatus. */ 339 private boolean mayChangeStatusTo(EventStatus newStatus) 340 { 341 boolean allowed = 342 getExecutionStatus().validNextStatuses().contains(newStatus); 343 344 //Limited allowable statuses for test blocks 345 if (allowed && isTest()) 346 { 347 allowed = newStatus.equals(NOT_READY_TO_BE_SCHEDULED) || 348 newStatus.equals(CANCELED); 349 } 350 351 return allowed; 352 } 353 354 /* (non-Javadoc) 355 * @see edu.nrao.sss.model.project.ScheduleEntry#getExecutionStatus() 356 */ 357 public EventStatus getExecutionStatus() 358 { 359 return execStatus; 360 } 361 362 @XmlElement 363 @SuppressWarnings("unused") //Used by Hibernate & JAXB 364 private void setExecutionStatus(EventStatus newStatus) 365 { 366 execStatus = (newStatus == null) ? NOT_READY_TO_BE_SCHEDULED : newStatus; 367 } 368 369 /* (non-Javadoc) 370 * @see edu.nrao.sss.model.project.ScheduleEntry#getExecutionStatusChangeDate() 371 */ 372 public Date getExecutionStatusChangeDate() 373 { 374 return execStatusChangeDate; 375 } 376 377 @XmlElement 378 @SuppressWarnings("unused") //Used by Hibernate & JAXB 379 private void setExecutionStatusChangeDate(Date newDate) 380 { 381 execStatusChangeDate = (newDate == null) ? new Date() : newDate; 382 } 383 384 /* (non-Javadoc) 385 * @see ScheduleEntry#updateStatusSubmit() 386 */ 387 public void updateStatusSubmit() 388 throws IllegalTransitionException 389 { 390 changeStatusTo(NOT_YET_SCHEDULED); 391 } 392 393 /* (non-Javadoc) 394 * @see ScheduleEntry#updateStatusSchedule() 395 */ 396 public void updateStatusSchedule() 397 throws IllegalTransitionException 398 { 399 changeStatusTo(SCHEDULED_BUT_NOT_STARTED); 400 } 401 402 /* (non-Javadoc) 403 * @see ScheduleEntry#updateStatusExecute() 404 */ 405 public void updateStatusExecute() 406 throws IllegalTransitionException 407 { 408 changeStatusTo(EventStatus.IN_PROGRESS); 409 } 410 411 /* (non-Javadoc) 412 * @see ScheduleEntry#updateStatusComplete() 413 */ 414 public void updateStatusComplete() 415 throws IllegalTransitionException 416 { 417 changeStatusTo(COMPLETED); 418 } 419 420 /* (non-Javadoc) 421 * @see ScheduleEntry#updateStatusFail() 422 */ 423 public void updateStatusFail() 424 throws IllegalTransitionException 425 { 426 changeStatusTo(CANCELED); 427 428 wasAborted = true; 429 430 //If this run failed, put siblings on-hold 431 if (schedBlock != null) 432 schedBlock.hold(1); 433 } 434 435 /* (non-Javadoc) 436 * @see edu.nrao.sss.model.project.ScheduleEntry#cancel() 437 */ 438 public void updateStatusCancel() 439 throws IllegalTransitionException 440 { 441 changeStatusTo(CANCELED); 442 } 443 444 /* (non-Javadoc) 445 * @see edu.nrao.sss.model.project.ScheduleEntry#reschedule() 446 */ 447 public void updateStatusReschedule() 448 throws IllegalTransitionException 449 { 450 changeStatusTo(NOT_YET_SCHEDULED); 451 } 452 453 /* (non-Javadoc) 454 * @see edu.nrao.sss.model.project.ScheduleEntry#hold() 455 */ 456 public void updateStatusHold() 457 throws IllegalTransitionException 458 { 459 changeStatusTo(ON_HOLD); 460 } 461 462 /* (non-Javadoc) 463 * @see edu.nrao.sss.model.project.ScheduleEntry#release() 464 */ 465 public void updateStatusRelease() 466 throws IllegalTransitionException 467 { 468 changeStatusTo(NOT_YET_SCHEDULED); 469 } 470 471 /** Returns <i>true</i> if this block was canceled while in progress. */ 472 boolean wasAborted() 473 { 474 return wasAborted; 475 } 476 477 /** 478 * Will attempt to put this EB and any of its eligible siblings on hold. 479 * If neither this EB nor any of its siblings may be held, this method 480 * will do nothing. 481 */ 482 public void holdThisAndSiblings() 483 { 484 if (schedBlock != null) 485 schedBlock.hold(0); 486 else 487 try 488 { 489 this.updateStatusHold(); 490 } 491 catch (IllegalTransitionException ex) 492 { 493 //One of those rare times when we can snuff out an exception. 494 //This method promises a "best effort" and does nothing if 495 //nothing can be done. 496 } 497 } 498 499 /** 500 * Will attempt to release this EB and any of its eligible siblings from 501 * the on-hold status. 502 * If neither this EB nor any of its siblings may be released, this method 503 * will do nothing. 504 */ 505 public void releaseThisAndSiblings() 506 { 507 if (schedBlock != null) 508 schedBlock.release(); 509 else 510 try 511 { 512 this.updateStatusRelease(); 513 } 514 catch (IllegalTransitionException ex) 515 { 516 //One of those rare times when we can snuff out an exception. 517 //This method promises a "best effort" and does nothing if 518 //nothing can be done. 519 } 520 } 521 522 //============================================================================ 523 // XML 524 //============================================================================ 525 526 /** 527 * Returns an XML representation of this execution block. 528 * @return an XML representation of this execution block. 529 * @throws JAXBException if anything goes wrong during the conversion to XML. 530 * @see #writeAsXmlTo(Writer) 531 */ 532 public String toXml() throws JAXBException 533 { 534 return JaxbUtility.getSharedInstance().objectToXmlString(this); 535 } 536 537 /** 538 * Writes an XML representation of this execution block to {@code writer}. 539 * @param writer the device to which XML is written. 540 * @throws JAXBException if anything goes wrong during the conversion to XML. 541 */ 542 public void writeAsXmlTo(Writer writer) throws JAXBException 543 { 544 JaxbUtility.getSharedInstance().writeObjectAsXmlTo(writer, this, null); 545 } 546 547 /** 548 * Creates a new execution block from the XML data in the given file. 549 * 550 * @param xmlFile the name of an XML file. This method will attempt to locate 551 * the file by using {@link Class#getResource(String)}. 552 * 553 * @return a new execution block from the XML data in the given file. 554 * 555 * @throws FileNotFoundException if the XML file cannot be found. 556 * 557 * @throws JAXBException if the schema file used (if any) is malformed, if 558 * the XML file cannot be read, or if the XML file is not 559 * schema-valid. 560 * 561 * @throws XMLStreamException if there is a problem opening the XML file, 562 * if the XML is not well-formed, or for some other 563 * "unexpected processing conditions". 564 */ 565 public static ExecutionBlock fromXml(String xmlFile) 566 throws JAXBException, XMLStreamException, FileNotFoundException 567 { 568 return JaxbUtility.getSharedInstance() 569 .xmlFileToObject(xmlFile, ExecutionBlock.class); 570 } 571 572 /** 573 * Creates a new execution block based on the XML data read from 574 * {@code reader}. 575 * 576 * @param reader the source of the XML data. 577 * If this value is <i>null</i>, <i>null</i> is returned. 578 * 579 * @return a new execution block based on the XML data read from 580 * {@code reader}. 581 * 582 * @throws XMLStreamException if the XML is not well-formed, 583 * or for some other "unexpected processing conditions". 584 * 585 * @throws JAXBException if anything else goes wrong during the 586 * transformation. 587 */ 588 public static ExecutionBlock fromXml(Reader reader) 589 throws JAXBException, XMLStreamException 590 { 591 return JaxbUtility.getSharedInstance() 592 .readObjectAsXmlFrom(reader, ExecutionBlock.class, null); 593 } 594 595 //============================================================================ 596 // 597 //============================================================================ 598 599 /** 600 * Returns a copy of this execution block. 601 * <p> 602 * For the most part this block is deeply cloned. 603 * There are, however, a few exceptions to the deep copy 604 * philosophy:</p> 605 * <ol> 606 * <li>The ID will be set to 607 * {@link Identifiable#UNIDENTIFIED}.</li> 608 * <li>The xmlId will be unique.</li> 609 * <li>This execution block will belong to no scheduling block.</li> 610 * </ol> 611 * <p> 612 * If anything goes wrong during the cloning procedure, 613 * a {@code RuntimeException} will be thrown.</p> 614 */ 615 @Override 616 public ExecutionBlock clone() 617 { 618 ExecutionBlock clone = null; 619 620 try 621 { 622 //This line takes care of the primitive fields properly 623 clone = (ExecutionBlock)super.clone(); 624 625 //We do NOT want the clone to have the same ID as the original. 626 //The ID is here for the persistence layer; it is in charge of 627 //setting IDs. To help it, we put the clone's ID in the uninitialized 628 //state. 629 clone.id = Identifiable.UNIDENTIFIED; 630 631 clone.xmlId = "execBlock-" + UUID.randomUUID().toString(); 632 633 clone.schedBlock = null; 634 clone.execStatusChangeDate = (Date)this.execStatusChangeDate.clone(); 635 clone.execScript = new StringBuilder(this.execScript); 636 } 637 catch (Exception ex) 638 { 639 throw new RuntimeException(ex); 640 } 641 642 return clone; 643 } 644 645 /** 646 * Returns <i>true</i> if {@code o} is equal to this execution block. 647 * <p> 648 * In order to be equal to this block, {@code o} must be non-null 649 * and of the same class as this block. Equality is determined by examining 650 * the equality of corresponding attributes, with the following exceptions, 651 * which are ignored when assessing equality:</p> 652 * <ol> 653 * <li>id</li> 654 * <li>xmlId</li> 655 * </ol> 656 */ 657 @Override 658 public boolean equals(Object o) 659 { 660 //Quick exit if o is null 661 if (o == null) 662 return false; 663 664 //Quick exit if o is this 665 if (o == this) 666 return true; 667 668 //Quick exit if classes are different 669 if (!o.getClass().equals(this.getClass())) 670 return false; 671 672 //A safe cast if we get this far 673 ExecutionBlock other = (ExecutionBlock)o; 674 675 //Attributes that we INTENTIONALLY DO NOT COMPARE: 676 // id, xmlId, schedBlock 677 678 //OTHER ATTRIBUTES 679 return 680 other.wasAborted == this.wasAborted && 681 other.execStatus.equals (this.execStatus) && 682 other.execStatusChangeDate.equals (this.execStatusChangeDate) && 683 other.comments.equals (this.comments) && 684 other.execScript.length() == this.execScript.length() && 685 other.execScript.toString().equals(this.execScript.toString()); 686 687 //StringBuilder.equals uses == 688 } 689 690 /* (non-Javadoc) 691 * @see java.lang.Object#hashCode() 692 */ 693 @Override 694 public int hashCode() 695 { 696 //Taken from the Effective Java book by Joshua Bloch. 697 //The constants 17 & 37 are arbitrary & carry no meaning. 698 int result = 17; 699 700 //You MUST keep this method in sync w/ the equals method 701 result = 37 * result + Boolean.valueOf(wasAborted).hashCode(); 702 result = 37 * result + execStatus.hashCode(); 703 result = 37 * result + execStatusChangeDate.hashCode(); 704 result = 37 * result + comments.hashCode(); 705 result = 37 * result + execScript.toString().hashCode(); 706 707 return result; 708 } 709 710 //============================================================================ 711 // 712 //============================================================================ 713 //This is here for quick manual testing 714 /* 715 public static void main(String[] args) 716 { 717 ProjectBuilder builder = new ProjectBuilder(); 718 builder.setIdentifiers(true); 719 720 ExecutionBlock eb = builder.makeExecutionBlock(); 721 722 try 723 { 724 eb.writeAsXmlTo(new java.io.PrintWriter(System.out)); 725 } 726 catch (JAXBException ex) 727 { 728 System.out.println("Trouble w/ eb.toXml. Msg:"); 729 System.out.println(ex.getMessage()); 730 ex.printStackTrace(); 731 732 System.out.println("Attempting to write XML w/out schema verification:"); 733 JaxbUtility.getSharedInstance().setLookForDefaultSchema(false); 734 try 735 { 736 eb.writeAsXmlTo(new java.io.PrintWriter(System.out)); 737 } 738 catch (JAXBException ex2) 739 { 740 System.out.println("Still had trouble w/ eb.toXml. Msg:"); 741 System.out.println(ex.getMessage()); 742 ex.printStackTrace(); 743 } 744 } 745 } 746 */ 747 }