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    }