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.ArrayList;
007    import java.util.Collection;
008    import java.util.Date;
009    import java.util.Collections;
010    import java.util.HashSet;
011    import java.util.List;
012    import java.util.Set;
013    
014    import javax.xml.bind.JAXBException;
015    import javax.xml.bind.annotation.XmlAttribute;
016    import javax.xml.bind.annotation.XmlElement;
017    import javax.xml.bind.annotation.XmlElementWrapper;
018    import javax.xml.bind.annotation.XmlRootElement;
019    import javax.xml.bind.annotation.XmlTransient;
020    import javax.xml.bind.annotation.XmlType;
021    import javax.xml.stream.XMLStreamException;
022    
023    import edu.nrao.sss.model.RepositoryException;
024    import edu.nrao.sss.model.UserAccountable;
025    import edu.nrao.sss.model.proposal.Proposal;
026    import edu.nrao.sss.model.proposal.ProposalProvider;
027    import edu.nrao.sss.model.resource.TelescopeType;
028    import edu.nrao.sss.util.EventSetStatus;
029    import edu.nrao.sss.util.EventStatus;
030    import edu.nrao.sss.util.Identifiable;
031    import edu.nrao.sss.util.JaxbUtility;
032    import edu.nrao.sss.validation.FailureSeverity;
033    import edu.nrao.sss.validation.ValidationException;
034    import edu.nrao.sss.validation.ValidationFailure;
035    
036    /**
037     * A scientifically independent subset of the observations set forth in
038     * a {@link Proposal}.
039     * <p>
040     * An approved {@code Proposal} may lead to one or more {@code Project}s.
041     * These projects are created by the Observing Program Committee.
042     * Different projects from the same proposal may have different scientific
043     * ratings.
044     * A proposal is typically split into projects when time is requested on
045     * different telescopes. There is usually a one-to-one corespondence between
046     * project and telescope within a given proposal.</p>
047     * <p>
048     * <b>Version Info:</b>
049     * <table style="margin-left:2em">
050     *   <tr><td>$Revision: 2277 $</td>
051     *   <tr><td>$Date: 2009-04-29 11:19:38 -0600 (Wed, 29 Apr 2009) $</td>
052     *   <tr><td>$Author: dharland $</td>
053     * </table></p>
054     *  
055     * @author David M. Harland
056     * @since 2006-02-24
057     */
058    @XmlRootElement
059    @XmlType(propOrder={"projectCode",
060                        "createdBy","createdOn","lastUpdatedBy","lastUpdatedOn",
061                        "executionStatus", "test", "telescope",
062                        "title", "editor", "proposalCode", "projectType",
063                        "scientificPriority", "allocatedTime", "comments",
064                        "progBlocks"})
065    public class Project
066      implements Identifiable, UserAccountable, Cloneable
067    {
068      static final String NO_PROJECT_CODE  = "[None]";
069      static final String NO_PROPOSAL_CODE = "[None]";
070      static final String UNINIT_TITLE     = "[New Project]";
071      
072      private static final String NO_COMMENTS = "";
073    
074      //IDENTIFICATION
075      private Long   id;            //A unique identifier for the persistence layer.
076      private String projectCode;   //This project's identifier; a problem-domain concept.
077      private String title;         //This project's title; c/b diff than proposal's title.
078      
079      //USER TRACKING
080      private Long createdBy;      //This is a user ID
081      private Date createdOn;
082      private Long lastUpdatedBy;  //This is a user ID
083      private Date lastUpdatedOn;
084      private Long editor;         //This is a user ID
085    
086      //CONTAINER & CONTAINED OBJECTS
087      private Proposal           proposal;
088      private String             proposalCode;
089      private List<ProgramBlock> progBlocks;
090      
091      //OTHER ATTRIBUTES
092      private boolean            isTest;
093      private ProjectType        projectType;
094      private EventSetStatus     executionStatus;
095      private TelescopeType      telescope;
096      private ScientificPriority scientificPriority;
097      private String             comments;
098      //TODO 1. Use TimeDuration  2. Stop storing timeUsedSoFar
099      private double             allocatedTime;  //In whole & fractional hours
100      private double             timeUsedSoFar;  //In whole & fractional hours
101      
102      //These instance variables are not persisted
103      private ProposalProvider   proposalProvider;
104    
105      //============================================================================
106      // INSTANCE CREATION & INITIALIZATION
107      //============================================================================
108    
109      /**
110       * Creates an instance of a project that is appropriate for the given
111       * telescope.
112       * The new project's telescope will have been communicated to
113       * the project via its {@link #setTelescope(TelescopeType)
114       * setTelescope} method before it is returned.
115       * 
116       * @param telescope the telescope for which a new project is desired.
117       *                  If {@code telescope} is <i>null</i>, it will be
118       *                  treated as if it had been
119       *                  {@link edu.nrao.sss.model.resource.TelescopeType#getDefault()}.
120       * 
121       * @return a new project for {@code telescope}.
122       */
123      public static Project createProject(TelescopeType telescope)
124      {
125        if (telescope == null)
126          telescope = TelescopeType.getDefault();
127        
128        //At this time we have no subclasses of project, so create the same
129        //kind of project no matter what kind of telescope we're passed.
130        Project newProject = new Project();
131        
132        newProject.setTelescope(telescope);
133        
134        return newProject;
135      }
136    
137      /** Creates a new instance. */
138      public Project()
139      {
140        progBlocks = new ArrayList<ProgramBlock>();
141        
142        initialize();
143      }
144      
145      /** Initializes the instance variables of this class.  */
146      private void initialize()
147      {
148        id                 = Identifiable.UNIDENTIFIED;
149        projectCode        = NO_PROJECT_CODE;
150        title              = UNINIT_TITLE;
151    
152        createdBy          = UserAccountable.NULL_USER_ID;
153        createdOn          = new Date();
154        lastUpdatedBy      = UserAccountable.NULL_USER_ID;
155        lastUpdatedOn      = new Date();
156        editor             = UserAccountable.NULL_USER_ID;
157    
158        proposal           = null;
159        proposalCode       = NO_PROPOSAL_CODE;
160        proposalProvider   = null;
161        
162        isTest             = true;
163        projectType        = ProjectType.getDefault();
164        executionStatus    = EventSetStatus.NOT_YET_SCHEDULED;
165        telescope          = TelescopeType.getDefault();
166        scientificPriority = ScientificPriority.UNKNOWN;
167        allocatedTime      = 0.0;
168        timeUsedSoFar      = 0.0;
169        comments           = NO_COMMENTS;
170      }
171      
172      /**
173       *  Resets this project to its initial state.  A reset project has the same
174       *  state as a new project. 
175       */
176      public void reset()
177      {
178        initialize();
179        
180        removeAllProgramBlocks();
181      }
182    
183      //============================================================================
184      // IDENTIFICATION
185      //============================================================================
186      
187      /* (non-Javadoc)
188       * @see edu.nrao.sss.model.util.Identifiable#getId()
189       */
190      @XmlAttribute
191      public Long getId()
192      {
193        return id;
194      }
195      
196      void setId(Long id)
197      {
198        this.id = id;
199      }
200    
201      /**
202       * Resets this project's id to UNIDENTIFIED and calls all of it's
203       * ProgramBlock's clearId() methods.
204       */
205      public void clearId()
206      {
207        id = Identifiable.UNIDENTIFIED;
208    
209        for (ProgramBlock pb : getProgramBlocks())
210          pb.clearId();
211      }
212    
213      /**
214       * Sets this project's NRAO code.
215       * <p>
216       * Note that this is <i>not</i> necessarily the identifier that the
217       * persistence layer will use to uniquely identify projects in the
218       * data store.  (See {@link #getId()} for that concept.)  Instead,
219       * this is NRAO's identifier for this project.</p>
220       * 
221       * @param newCode this project's NRAO identifier.
222       */
223      public void setProjectCode(String newCode)
224      {
225        projectCode = (newCode == null) ? NO_PROJECT_CODE : newCode;
226      }
227      
228      /**
229       * Returns this project's NRAO code.
230       * 
231       * @return this project's NRAO code.
232       * 
233       * @see #setProjectCode(String)
234       */
235      public String getProjectCode()
236      {
237        return projectCode;
238      }
239    
240      /**
241       * Sets the title of this project.  Note that this project's
242       * title may be different than the title of the
243       * {@link Proposal} that led to this project.
244       * 
245       * @param newTitle the title of this project.
246       */
247      public void setTitle(String newTitle)
248      {
249        title = (newTitle == null) ? UNINIT_TITLE : newTitle;
250      }
251      
252      /**
253       * Returns the title of this project.
254       * 
255       * @return the title of this project.
256       */
257      public String getTitle()
258      {
259        return title;
260      }
261    
262      //============================================================================
263      // CONTAINER (Proposal)
264      //============================================================================
265    
266      /**
267       * Sets the proposal that spawned this project.
268       * <p>
269       * If {@code proposal} is the same as {@code this.getProposal()},
270       * this method will do nothing.  Otherwise a reference to {@code proposal}
271       * is saved in this project.</p>
272       * <p>
273       * Consequences of Changing a Project's Proposal:
274       * <ul>
275       *   <li>All existing sources in the project are removed and replaced
276       *       with the sources from the proposal.</li>
277       *   <li>All program blocks are removed.</li>
278       * </ul></p>
279       * 
280       * @param proposal the proposal that spawned this project.
281       */
282      public void setProposal(Proposal proposal)
283      {
284        //Quick exit if this project is already linked to incoming proposal 
285        if (this.proposalIsAlreadySetTo(proposal))
286          return;
287    
288        //Save the proposal & its code.  Note: The proposal has no link to its
289        //projects, so we do not need to update it. 
290        this.proposal = proposal;
291        
292        if (proposal != null)
293        {
294          this.proposalCode = proposal.getProposalCode();
295    
296          //TODO Decide if this is really what we want to do:
297          setEditor(proposal.getEditor());
298        }
299        else
300        {
301          this.proposalCode = NO_PROPOSAL_CODE;
302        }
303        
304        //Since we're dealing w/ a new proposal, drop the current program blocks
305        progBlocks.clear();
306      }
307      
308      private boolean proposalIsAlreadySetTo(Proposal proposal)
309      {
310        //If both are null, they're equal
311        if ((proposal == null) && (proposal == this.proposal))
312          return true;
313        
314        if ((proposal != null) && proposal.equals(this.proposal))
315          return true;
316        
317        return false;
318      }
319    
320      /**
321       * Returns the proposal that spawned this project.  Note that the
322       * returned proposal may have spawned additional projects.
323       * <p>
324       * <i>Note to Developers:</i> This method may be expensive.  If
325       * all you need is the proposal's ID code, the
326       * {@link #getProposalCode()} method is more efficient.  Of course,
327       * if you need extensive information from the proposal,
328       * you should use this method.</p>
329       * <p>
330       * Note that it is possible for this project to not yet have
331       * a proposal.  In this situation, the return value will be
332       * <i>null</i>.  Also, if there are any problems encountered
333       * while using the proposal repository, <i>null</i> will
334       * be returned.</p>
335       * 
336       * @return the proposal that spawned this project.
337       * 
338       * @see #getProposalCode()
339       * @see #hasProposal()
340       */
341      @XmlTransient
342      public Proposal getProposal()
343      {
344        //Just-in-time fetch of the proposal
345        if (null == proposal)
346          proposal = fetchProposal();
347    
348        return proposal;
349      }
350      
351      /**
352       * Returns <i>true</i> if this project has a non-null proposal.
353       * <p>
354       * The only times this method should return <i>false</i> are:
355       * <ol>
356       *   <li>This project has just been created and its proposal
357       *       has not yet been set.</li>
358       *   <li>A client set this project's proposal to <i>null</i></li>
359       * </ol></p>
360       * 
361       * @return <i>true</i> if this project has a non-null proposal.
362       */
363      public boolean hasProposal()
364      {
365        return !NO_PROPOSAL_CODE.equals(proposalCode);
366      }
367      
368      /**
369       * Tells this project where to look for proposals if it does not
370       * already have one.
371       * @param newProvider a provider of proposals.
372       */
373      public void setProposalProvider(ProposalProvider newProvider)
374      {
375        //We're intentionally allowing a null value here
376        proposalProvider = newProvider;
377      }
378      
379      /**
380       * Uses a provider to find the proposal to which this project belongs.
381       * 
382       * @return the proposal represented by this project's
383       *         proposal ID attribute.
384       */
385      private Proposal fetchProposal()
386      {
387        Proposal result = null;
388        
389        //Try to fetch the proposal ONLY if we have an ID for it.
390        if (!NO_PROPOSAL_CODE.equals(proposalCode) &&
391            (null != proposalProvider))
392        {
393          try 
394          {
395            result = proposalProvider.findByCode(proposalCode);
396          }
397          catch (RepositoryException de)
398          {
399            //if we get an exception, we want to return null (which is what it is
400            //initialized to).
401          }
402        }
403        
404        return result;
405      }
406      
407      /**
408       * Returns the NRAO code of the proposal that spawned this project.
409       * <p>
410       * Calling this method instead of {@code getProposal().getPropId()}
411       * may be more efficient.</p>
412       * 
413       * @return the NRAO code of the proposal that spawned this project.
414       * 
415       * @see #getProposal()
416       */
417      @XmlElement
418      public String getProposalCode()
419      {
420        return proposalCode;
421      }
422      
423      public void setProposalCode(String newCode)
424      {
425        proposalCode = (newCode == null) ? NO_PROPOSAL_CODE : newCode;
426      }
427    
428      //============================================================================
429      // PROGRAM BLOCKS
430      //============================================================================
431      
432      /**
433       * Creates and returns a new program block that is suitable for use with
434       * this project.
435       * The returned program block has <i>not</i> been added to
436       * this project.
437       * 
438       * @return a new program block that can later be added to this project.
439       */
440      public ProgramBlock createProgramBlock()
441      {
442        return new ProgramBlock();
443      }
444      
445      /**
446       * Adds the given program block to this project.
447       * <p>
448       * If {@code progBlock} is already part of this project,
449       * or if it is <i>null</i>, no action is taken.
450       * Otherwise it is added to this project, removed from
451       * the project to which it had been attached (if any),
452       * and updated so that it knows it belongs to this project.</p>
453       * 
454       * @param progBlock the program block to be added to this project.
455       */
456      public void addProgramBlock(ProgramBlock progBlock)
457      {
458        addProgramBlock(progBlocks.size(), progBlock);
459      }
460    
461      /**
462       * Adds the given program block to this project at index {@code idx}.
463       * <p>
464       * If {@code progBlock} is already part of this project,
465       * or if it is <i>null</i>, no action is taken.
466       * Otherwise it is added to this project, removed from
467       * the project to which it had been attached (if any),
468       * and updated so that it knows it belongs to this project.</p>
469       * 
470       * @param idx the index in our list of program blocks at which to add {@code progBlock}.
471       * @param progBlock the program block to be added to this project.
472       */
473      public void addProgramBlock(int idx, ProgramBlock progBlock)
474      {
475        //Quick exit if progBlock is null, or if its project is this project.
476        //(Intentional use of "==" here.)
477        if ((progBlock == null) || (progBlock.getProject() == this))
478          return;
479        
480        if (idx < 0 || idx > progBlocks.size())
481        {
482          throw new IndexOutOfBoundsException(idx +
483            " is not within the bounds: (0, " + progBlocks.size() + ")");
484        }
485    
486        //TODO Make sure progBlock is of correct variety
487        
488        //Remove progBlock from its former project
489        Project formerProject = progBlock.getProject();
490        if (formerProject != null)
491          formerProject.progBlocks.remove(progBlock);
492    
493        //Add progBlock to our collection
494        progBlocks.add(idx, progBlock);
495        
496        //Tell progBlock it now belongs to this project
497        progBlock.simplySetProject(this);
498        
499        //Add any direct prereqs and/or adjust direct prereq references
500        addOrAdjustDirectPrereqsOf(progBlock);
501      }
502      
503      //The incoming PB might have prerequisites.  If a given prereq is not
504      //already part of this project, we need to add it.  If a given prereq is
505      //EQUAL TO a PB already in this project, we need to tell the incoming PB
506      //to use the equivalent PB in this project as a prereq.
507      //TODO Reexamine that last statement.  Are we saying this Project cannot
508      //     hold multiple value-equal PBs?
509      private void addOrAdjustDirectPrereqsOf(ProgramBlock pb)
510      {
511        //Make a new set to avoid concurrent modification exceptions
512        Set<ProgramBlock> prereqs =
513          new HashSet<ProgramBlock>(pb.getDirectPrerequisites());
514        
515        for (ProgramBlock prereq : prereqs)
516        {
517          int index = progBlocks.indexOf(prereq);
518          
519          if (index >= 0) //this project has a PB equal to prereq
520          {
521            ProgramBlock equalPB = progBlocks.get(index);
522            
523            //The PB in this project is equal to, but not the same object as, prereq.
524            //We need pb's list of prereqs to use the equiv PB from project.
525            if (prereq != equalPB)
526            {
527              pb.removePrerequisite(prereq);
528              pb.addPrerequisite(equalPB);
529            }
530            //else prereq == equalPB, so do nothing more
531          }
532          else //this project does NOT have PB equal to prereq
533          {
534            //If the prereq is in another project, we do not disturb that project.
535            //Instead, we copy the prereq and adjust the reference to prereq
536            //held by pb.  This also has the effect of adding pb to this project.
537            if (prereq.hasProject())
538            {
539              ProgramBlock copyOfPrereq = prereq.clone();
540              copyOfPrereq.setProject(null);
541              pb.removePrerequisite(prereq);
542              pb.addPrerequisite(copyOfPrereq);
543            }
544            //If the prereq is not already in another project, we just add it here
545            else
546            {
547              addProgramBlock(prereq);
548            }
549          }
550        }
551      }
552      
553      /**
554       * Removes the given program block from this project.
555       * <p>
556       * If {@code progBlock} is <i>null</i>, or if it does
557       * not belong to this project, nothing happens.
558       * If it is a prerequisite of one or more of the program
559       * blocks held by this project, an exception is thrown
560       * and it is not removed from this project.
561       * Otherwise, {@code progBlock} is removed from this
562       * project and has its project attribute set to
563       * <i>null</i>.</p>
564       * 
565       * @param progBlock the program block to be removed.
566       */
567      public void removeProgramBlock(ProgramBlock progBlock)
568      {
569        //Quick exit if progBlock is null or does not belong to this project
570        if ((progBlock == null) || (progBlock.getProject() != this))
571          return;
572        
573        //Throw exception if schedBlock is a prereq of another SB
574        for (ProgramBlock pb : progBlocks)
575        {
576          if ((pb != progBlock) && progBlock.isDirectPrerequisiteOf(pb))
577            throw prereqRemovalError(progBlock);
578        }
579        
580        //Remove the progBlock from our collection
581        progBlocks.remove(progBlock);
582        
583        //Tell progBlock that it belongs to no project
584        progBlock.simplySetProject(null);
585      }
586    
587      /** Constructs and returns an exception. */
588      private ValidationException prereqRemovalError(ProgramBlock target)
589      {
590        StringBuilder buff = new StringBuilder("Program block '");
591        
592        buff.append(target.getName())
593            .append("' is a direct prerequisite of the following PBs:");
594    
595        for (ProgramBlock pb : progBlocks)
596        {
597          if ((pb != target) && target.isDirectPrerequisiteOf(pb))
598            buff.append(' ').append(pb.getName()).append(',');
599        }
600        
601        int buffLen = buff.length();
602        buff.replace(buffLen-1, buffLen, ".");
603        
604        buff.append("  Because of this, ").append(target.getName())
605            .append(" was not removed.  Please first remove it from the ")
606            .append("prerequisite lists of each of the above PBs.");
607        
608        ValidationFailure failure =
609          new ValidationFailure(buff.toString(), "Attempt to delete prereq PB.",
610                                FailureSeverity.ERROR, target,
611                                this.getClass().getName(),
612                                "prereqRemovalError");
613        
614        ValidationException exception =
615          new ValidationException("Cannot remove Program Block.");
616        
617        exception.getFailures().add(failure);
618        
619        return exception;
620      }
621      
622      /**
623       * Removes all program blocks from this project.
624       * Each program block is notified that it no longer has
625       * a containing project.
626       */
627      public void removeAllProgramBlocks()
628      {
629        for (ProgramBlock progBlock : progBlocks)
630          progBlock.simplySetProject(null);
631        
632        progBlocks.clear();
633      }
634    
635      /**
636       * Returns the program blocks that belong to this project.
637       * <p>
638       * The returned {@code List} is a copy of the one held internally
639       * by this project, so changes made to it will not be reflected herein.</p>
640       * 
641       * @return the program blocks that belong to this project.
642       */
643      public List<ProgramBlock> getProgramBlocks()
644      {
645        return new ArrayList<ProgramBlock>(progBlocks);
646      }
647      
648      /**
649       * Returns a list of this project's program blocks sorted such that
650       * any prerequisites of the block at index i are at in indices greater than i.
651       * <p>
652       * Note that the sort determines only how prerequisites and things
653       * dependent on them are place relative to one another.  For example,
654       * imagine four blocks A, B, C, and D.  C has A and B as prerequisites
655       * and no other block has prerequisites.
656       * The returned list will place C in a lower index than A and B.
657       * However, we know nothing about how A and B will be placed relative
658       * to each other, nor do we know ahead of time where D will be placed.</p>
659       * 
660       * @return program blocks sorted by prerequisite relationships.
661       */
662      public List<ProgramBlock> getProgramBlocksSortedByPrereqs()
663      {
664        List<ProgramBlock> pbs = getProgramBlocks();
665        
666        Collections.sort(pbs, ProgramBlock.getPrequisiteComparator());
667        
668        return pbs;
669      }
670      
671      /** Returns the position of progBlock in our list using "==". */
672      private int getProgBlockIndex(ProgramBlock progBlock)
673      {
674        int pbCount = progBlocks.size();
675        int index   = -1;
676        
677        for (int p=0; p < pbCount; p++)
678        {
679          //Intentional use of "=="
680          if (progBlocks.get(p) == progBlock)
681          {
682            //Break at first match; "add" method does not allow multiple
683            //entries of same instance, so th is is safe.
684            index = p;
685            break;
686          }
687        }
688    
689        return index;
690      }
691    
692      /**
693       * Returns the schedule entries of this project that 
694       * have an execution status of {@code execStatus}.  Only the status of the
695       * entries is considered.  This method does not look, for example, at the
696       * {@link #isTest()} property.
697       * 
698       * @param execStatus the execution status of the schedule entries to be added
699       *                   to {@code destination}.
700       *                   
701       * @param destination the collection to which the schedule entries should be
702       *                    added.  If this collection is <i>null</i>, a new one
703       *                    will be created.  This collection is returned.
704       *                    
705       * @return the collection holding the schedule entries.  This will be either
706       *         {@code destination} or a new collection, if {@code destination} is
707       *         <i>null</i>.
708       *         
709       * @see #getReadyToScheduleEntries(Collection)
710       */
711      public Collection<ScheduleEntry> getScheduleEntries(
712                                         EventStatus               execStatus,
713                                         Collection<ScheduleEntry> destination)
714      {
715        if (destination == null)
716          destination = new ArrayList<ScheduleEntry>();
717        
718        for (ProgramBlock progBlock : progBlocks)
719          progBlock.getScheduleEntries(execStatus, destination);
720        
721        return destination;
722      }
723    
724      /**
725       * Returns the schedule entries of this project that are ready for
726       * scheduling.  If this is a test project, or if this
727       * project has no <tt>NOT_YET_SCHEDULED</tt> entries, the returned
728       * collection will be empty.
729       * 
730       * @param destination the collection to which the ready-to-be-scheduled
731       *                    entries should be added.  If this collection is
732       *                    <i>null</i>, a new one will be created.  This
733       *                    collection is returned.
734       *                    
735       * @return a collection of ready-to-be-scheduled entries, or an empty
736       *         collection.  The returned collection will be either
737       *         {@code destination} or a new collection, if {@code destination} is
738       *         <i>null</i>.
739       *         
740       * @see #getScheduleEntries(EventStatus, Collection)
741       */
742      public Collection<ScheduleEntry>
743        getReadyToScheduleEntries(Collection<ScheduleEntry> destination)
744      {
745        if (destination == null)
746          destination = new ArrayList<ScheduleEntry>();
747        
748        if (!isTest())
749          getScheduleEntries(EventStatus.NOT_YET_SCHEDULED, destination);
750        
751        return destination;
752      }
753    
754      @XmlElementWrapper(name="programBlocks")
755      @XmlElement(name="programBlock")
756      @SuppressWarnings("unused")  //JAXB use
757      private void setProgBlocks(ProgramBlock[] replacements)
758      {
759        progBlocks.clear();
760        
761        for (ProgramBlock pb : replacements)
762        {
763          pb.simplySetProject(this);
764          progBlocks.add(pb);
765        }
766      }
767      
768      @SuppressWarnings("unused")  //JAXB use
769      private ProgramBlock[] getProgBlocks()
770      {
771        return progBlocks.toArray(new ProgramBlock[progBlocks.size()]);
772      }
773    
774      //============================================================================
775      // STATUS
776      //============================================================================
777      
778      /**
779       * This method is here for persistence mechanisms such as Hibernate
780       * and JAXB.  It is NOT appropriate to let other objects set the
781       * status, because the status of this object is derived from that
782       * of contained objects.
783       */
784      @XmlElement
785      @SuppressWarnings("unused")
786      private void setExecutionStatus(EventSetStatus newStatus)
787      {
788        executionStatus = newStatus;
789      }
790    
791      /**
792       * Returns this project's execution status.
793       * 
794       * @return this project's execution status.
795       */
796      public EventSetStatus getExecutionStatus()
797      {
798        if (!executionStatus.isFinal())
799          recomputeStatus();
800        
801        return executionStatus;
802      }
803      
804      /**
805       * Sets this project's status based on the combined statuses of
806       * this project's program blocks.
807       */
808      private void recomputeStatus()
809      {
810        int execBlockCount = progBlocks.size();
811        
812        EventSetStatus[] statuses = new EventSetStatus[execBlockCount];
813        
814        for (int e=0; e < execBlockCount; e++)
815          statuses[e] = progBlocks.get(e).getExecutionStatus();
816        
817        executionStatus = EventSetStatus.createFrom(statuses);
818      }
819    
820      //============================================================================
821      // OTHER ATTRIBUTES
822      //============================================================================
823      
824      /**
825       * Sets the telescope on which this project will be performed.
826       * 
827       * @param telescope the telescope used by this project.
828       */
829      public void setTelescope(TelescopeType telescope)
830      {
831        if (!isTooLateToChangeTelescope())
832          this.telescope = (telescope == null) ? TelescopeType.getDefault() : telescope;
833      }
834      
835      /**
836       * Returns <i>true</i> if it is too late to change the telescope for this project.
837       * 
838       * @return <i>true</i> if it is too late to change the telescope for this project.
839       */
840      private boolean isTooLateToChangeTelescope()
841      {
842        //TODO Do we need this construct?  Rationale: once things like program blocks
843        //     and scheduling blocks are set up, changing the telescope could be a
844        //     big no-no.
845        //return !this.telescope.equals(TelescopeType.UNKNOWN);
846        return false;
847      }
848      
849      /**
850       * Returns the telescope used by this project.
851       * 
852       * @return telescope used by this project.
853       */
854      public TelescopeType getTelescope()
855      {
856        return telescope;
857      }
858    
859      /**
860       * Sets the scientific priority for this project.
861       * 
862       * @param newPriority the scientific priority for this project.
863       */
864      public void setScientificPriority(ScientificPriority newPriority)
865      {
866        scientificPriority = (newPriority == null) ? ScientificPriority.UNKNOWN
867                                                   : newPriority;
868      }
869      
870      /**
871       * Returns the scientific priority of this project.
872       * 
873       * @return the scientific priority of this project.
874       */
875      public ScientificPriority getScientificPriority()
876      {
877        return scientificPriority;
878      }
879    
880      /**
881       * Sets the amount of time allocated to this project
882       * by the <i>Time Allocation Committee</i>.
883       *
884       * @param allocatedTime the amount of time allocated to this project
885       *                      in whole and fractional hours.
886       */
887      public void setAllocatedTime(double allocatedTime)
888      {
889        if (allocatedTime >= 0.0)
890          this.allocatedTime = allocatedTime;
891      }
892    
893      /**
894       * Returns the amount of time allocated to this project
895       * by the <i>Time Allocation Committee</i>.
896       *
897       * @return the amount of time allocated to this project
898       *         in whole and fractional hours.
899       */
900      public double getAllocatedTime()
901      {
902        return allocatedTime;
903      }
904    
905      /**
906       * Declares this project as either a test or official project.
907       * The default state of a newly created project is as a test project.
908       * 
909       * @param projectIsTest <i>true</i> if this project is a test project.
910       */
911      public void setTest(boolean projectIsTest)
912      {
913        isTest = projectIsTest;
914      }
915      
916      /**
917       * Returns <i>true</i> if this project is a test, as opposed to
918       * official, project.
919       * @return <i>true</i> if this project is unofficial.
920       */
921      public boolean isTest()
922      {
923        return isTest;
924      }
925      
926      /**
927       * Sets the type of this project.
928       *
929       * @param type the type of this project.
930       */
931      public void setProjectType(ProjectType type)
932      {
933        this.projectType = (type == null) ? ProjectType.getDefault() : type;
934      }
935    
936      /**
937       * Returns the type of this project.
938       *
939       * @return the type of this project.
940       */
941      public ProjectType getProjectType()
942      {
943        return projectType;
944      }
945    
946      /**
947       * Returns the telescope time used so far for this project.
948       *
949       * @return the telescope time used so far for this project.
950       */
951      public double getTimeUsedSoFar()
952      {
953        return timeUsedSoFar;
954      }
955    
956      //============================================================================
957      // INTERFACE UserAccountable
958      //============================================================================
959      
960      public void setCreatedBy(Long userId)
961      {
962        createdBy = (userId == null) ? UserAccountable.NULL_USER_ID : userId;
963      }
964      
965      public void setCreatedOn(Date d)
966      {
967        if (d != null)
968          createdOn = d;
969      }
970    
971      public void setLastUpdatedBy(Long userId)
972      {
973        lastUpdatedBy = (userId == null) ? UserAccountable.NULL_USER_ID : userId;
974      }
975    
976      public void setLastUpdatedOn(Date d)
977      {
978        if (d != null)
979          lastUpdatedOn = d;
980      }
981    
982      public Long getCreatedBy()      { return createdBy;     }
983      public Date getCreatedOn()      { return createdOn;     }
984      public Long getLastUpdatedBy()  { return lastUpdatedBy; }
985      public Date getLastUpdatedOn()  { return lastUpdatedOn; }
986    
987      /**
988       * Sets the ID of the user who is authorized to edit this project.
989       * 
990       * @param userId the ID of the user who is authorized to edit this project.
991       */
992      public void setEditor(Long userId)
993      {
994        editor = (userId == null) ? UserAccountable.NULL_USER_ID : userId;
995      }
996      
997      /**
998       * Returns the ID of the user who is authorized to edit this project.
999       * 
1000       * @return the ID of the user who is authorized to edit this project.
1001       */
1002      public Long getEditor()  { return editor; }
1003    
1004      //============================================================================
1005      // COMMENTS
1006      //============================================================================
1007    
1008      /**
1009       * Sets comments about this project.
1010       * 
1011       * @param replacementComments
1012       *   free-form comments about this project.
1013       *   These comments replace all previously set comments.
1014       *   A <i>null</i> value will be replaced by the empty string (<tt>""</tt>).
1015       * 
1016       * @see #appendComments(String)
1017       */
1018      public void setComments(String replacementComments)
1019      {
1020        comments = (replacementComments == null) ? NO_COMMENTS : replacementComments;
1021      }
1022      
1023      /**
1024       * Adds additional comments to those already associated with this project.
1025       * 
1026       * @param additionalComments
1027       *   new, additional, comments about this project.
1028       *   
1029       * @see #setComments(String)
1030       */
1031      public void appendComments(String additionalComments)
1032      {
1033        if ((additionalComments != null) && (additionalComments.length() > 0))
1034        {
1035          if (!comments.equals(NO_COMMENTS))
1036            comments = comments + System.getProperty("line.separator");
1037          
1038          comments = comments + additionalComments;
1039        }
1040      }
1041    
1042      /**
1043       * Returns comments about this project.
1044       * The value returned is guaranteed to be non-null.
1045       * 
1046       * @return
1047       *   free-form comments about this project.
1048       *   
1049       * @see #appendComments(String)
1050       * @see #setComments(String)
1051       */
1052      public String getComments()
1053      {
1054        return comments;
1055      }
1056    
1057      //============================================================================
1058      // TEXT
1059      //============================================================================
1060      
1061      /**
1062       * Returns a text representation of this project.
1063       * The default form of the text is XML.  However, if anything goes wrong
1064       * during the conversion to XML, an alternate, and much abbreviated, form
1065       * will be returned.
1066       * 
1067       * @return a text representation of this project.
1068       */
1069      public String toString()
1070      {
1071        try {
1072          return toXml();
1073        }
1074        catch (Exception ex) {
1075          return toSummaryString();
1076        }
1077      }
1078      
1079      /**
1080       * Returns a short textual description of this project.
1081       * @return a short textual description of this project.
1082       */
1083      public String toSummaryString()
1084      {
1085        StringBuilder buff = new StringBuilder();
1086        
1087        buff.append("code=").append(projectCode);
1088        buff.append(", id=").append(id);
1089        buff.append(", title=").append(title);
1090        buff.append(", proposal=").append(proposalCode);
1091        
1092        return buff.toString();
1093      }
1094    
1095      /**
1096       * Returns an XML representation of this project.
1097       * @return an XML representation of this project.
1098       * @throws JAXBException if anything goes wrong during the conversion to XML.
1099       * @see #writeAsXmlTo(Writer)
1100       */
1101      public String toXml() throws JAXBException
1102      {
1103        return JaxbUtility.getSharedInstance().objectToXmlString(this);
1104      }
1105      
1106      /**
1107       * Writes an XML representation of this project to {@code writer}.
1108       * @param writer the device to which XML is written.
1109       * @throws JAXBException if anything goes wrong during the conversion to XML.
1110       */
1111      public void writeAsXmlTo(Writer writer) throws JAXBException
1112      {
1113        JaxbUtility.getSharedInstance().writeObjectAsXmlTo(writer, this, null);
1114      }
1115      
1116      /**
1117       * Creates a new project from the XML data in the given file.
1118       * 
1119       * @param xmlFile the name of an XML file.  This method will attempt to locate
1120       *                the file by using {@link Class#getResource(String)}.
1121       *                
1122       * @return a new project from the XML data in the given file.
1123       * 
1124       * @throws FileNotFoundException if the XML file cannot be found.
1125       * 
1126       * @throws JAXBException if the schema file used (if any) is malformed, if
1127       *           the XML file cannot be read, or if the XML file is not
1128       *           schema-valid.
1129       * 
1130       * @throws XMLStreamException if there is a problem opening the XML file,
1131       *           if the XML is not well-formed, or for some other
1132       *           "unexpected processing conditions".
1133       */
1134      public static Project fromXml(String xmlFile)
1135        throws JAXBException, XMLStreamException, FileNotFoundException
1136      {
1137        Project newProj = JaxbUtility.getSharedInstance().xmlFileToObject(xmlFile, Project.class);
1138        
1139        newProj.testScansForResourceFromJaxb();
1140        
1141        return newProj;
1142      }
1143      
1144      /**
1145       * Creates a new project based on the XML data read from {@code reader}.
1146       * 
1147       * @param reader the source of the XML data.
1148       *               If this value is <i>null</i>, <i>null</i> is returned.
1149       *               
1150       * @return a new project based on the XML data read from {@code reader}.
1151       * 
1152       * @throws XMLStreamException if the XML is not well-formed,
1153       *           or for some other "unexpected processing conditions".
1154       *           
1155       * @throws JAXBException if anything else goes wrong during the
1156       *           transformation.
1157       */
1158      public static Project fromXml(Reader reader)
1159        throws JAXBException, XMLStreamException
1160      {
1161        Project newProj = JaxbUtility.getSharedInstance()
1162                                     .readObjectAsXmlFrom(reader, Project.class, null);
1163        
1164        newProj.testScansForResourceFromJaxb();
1165        
1166        return newProj;
1167      }
1168    
1169      /**
1170       * @throws JAXBException
1171       *   if the any scan of this project has
1172       *   no resource and the useResourceOfPriorScan flag is false.
1173       */
1174      private void testScansForResourceFromJaxb() throws JAXBException
1175      {
1176        for (ProgramBlock pb : progBlocks)
1177          pb.testScansForResourceFromJaxb();
1178      }
1179    
1180      //============================================================================
1181      // 
1182      //============================================================================
1183      
1184      /**
1185       *  Returns a project that is a copy of this one.
1186       *  <p>
1187       *  For the most part this project is deeply cloned.  For example, the
1188       *  clone and this project will have their own lists of program blocks,
1189       *  and the program blocks in the clone will be clones of those in this
1190       *  project.  There are, however, a few exceptions to the deep copy
1191       *  philosophy:</p>
1192       *  <ol>
1193       *    <li>The ID will be set to
1194       *        {@link Identifiable#UNIDENTIFIED}.</li>
1195       *    <li>The proposal will refer to the same proposal that this project
1196       *        points to.</li>
1197       *    <li>The createdOn and lastUpdatedOn attributes will be set to the
1198       *        current system time.</li>
1199       *  </ol>
1200       *  <p>
1201       *  If anything goes wrong during the cloning procedure,
1202       *  a {@code RuntimeException} will be thrown.</p>
1203       */
1204      public Project clone()
1205      {
1206        Project clone = null;
1207    
1208        try
1209        {
1210          //This line takes care of the primitive fields properly
1211          clone = (Project)super.clone();
1212    
1213          //We do NOT want the clone to have the same ID as the original.
1214          //The ID is here for the persistence layer; it is in charge of
1215          //setting IDs.  To help it, we put the clone's ID in the uninitialized
1216          //state.
1217          clone.id = Identifiable.UNIDENTIFIED;
1218          
1219          clone.createdOn     = new Date();
1220          clone.lastUpdatedOn = clone.createdOn;
1221          
1222          //Clone the collection and partially clone the contained elements
1223          clone.progBlocks = new ArrayList<ProgramBlock>();
1224          for (ProgramBlock pb : this.progBlocks)
1225            clone.addProgramBlock(pb.cloneWithoutPrerequisites());
1226    
1227          //Recreate for the cloned SBs analogous prerequisite trees
1228          fixClonesSchedBlockPrereqs(clone);
1229        }
1230        catch (Exception ex)
1231        {
1232          throw new RuntimeException(ex);
1233        }
1234    
1235        return clone;
1236      }
1237      
1238      /** Helps the clone method get the clone's PB relationships straight. */
1239      private void fixClonesSchedBlockPrereqs(Project clonedProj)
1240      {
1241        //STRATEGY
1242        //Loop through all of our own PBs.  For each PB that has
1243        //direct prerequisites, note the positions in our list of
1244        //the prereqs.  The clone's PB at the same position as the
1245        //current PB should receive as direct prereqs the PBs at
1246        //the indices we found.
1247        
1248        List<ProgramBlock> pbsOfClone = clonedProj.getProgramBlocks();
1249        
1250        int pbCount = progBlocks.size();
1251        
1252        for (int p=0; p < pbCount; p++)
1253        {
1254          ProgramBlock myPb      = progBlocks.get(p);
1255          ProgramBlock myPbClone = pbsOfClone.get(p);
1256          
1257          for (ProgramBlock prereqOfMyPb : myPb.getDirectPrerequisites())
1258          {
1259            int prereqIndex = getProgBlockIndex(prereqOfMyPb);
1260            
1261            if (prereqIndex >= 0)
1262              myPbClone.addPrerequisite(pbsOfClone.get(prereqIndex));
1263          }
1264        }
1265      }
1266    
1267      /**
1268       * Returns <i>true</i> if {@code o} is equal to this project.
1269       * <p>
1270       * In order to be equal to this project, {@code o} must be non-null
1271       * and of the same class as this project. Equality is determined by examining
1272       * the equality of corresponding attributes, with the following exceptions,
1273       * which are ignored when assessing equality: 
1274       * <ol>
1275       *   <li>id</li>
1276       *   <li>proposal
1277       *       (but <i>not</i> proposal code, which is <i>not</i> ignored)</li>
1278       *   <li>createdOn</li>
1279       *   <li>createdBy</li>
1280       *   <li>lastUpdatedOn</li>
1281       *   <li>lastUpdatedBy</li>
1282       * </ol></p>
1283       */
1284      @Override
1285      public boolean equals(Object o)
1286      {
1287        //Quick exit if o is null
1288        if (o == null)
1289          return false;
1290        
1291        //Quick exit if o is this
1292        if (o == this)
1293          return true;
1294        
1295        //Quick exit if classes are different
1296        if (!o.getClass().equals(this.getClass()))
1297          return false;
1298        
1299        Project other = (Project)o;
1300        
1301        //Attributes that we INTENTIONALLY DO NOT COMPARE:
1302        //  id,
1303        //  proposal,
1304        //  createdOn, createdBy, lastUpdatedOn, lastUpdatedBy
1305        //  proposalProvider
1306    
1307        if (!other.projectCode.equals(this.projectCode)                   ||
1308             other.isTest != this.isTest                                  || 
1309            !other.title.equals(this.title)                               ||
1310            !other.editor.equals(this.editor)                             ||
1311            !other.proposalCode.equals(this.proposalCode)                 ||
1312            !other.projectType.equals(this.projectType)                   ||
1313            !other.getExecutionStatus().equals(this.getExecutionStatus()) ||
1314            !other.telescope.equals(this.telescope)                       ||
1315            !other.scientificPriority.equals(this.scientificPriority)     ||
1316            !other.comments.equals(this.comments)                         ||
1317             other.allocatedTime != this.allocatedTime                    ||
1318             other.timeUsedSoFar != this.timeUsedSoFar)
1319          return false;
1320    
1321        if (!other.progBlocks.equals(this.progBlocks))
1322          return false;
1323        
1324        //No differences found
1325        return true;
1326      }
1327      
1328      /* (non-Javadoc)
1329       * @see java.lang.Object#hashCode()
1330       */
1331      @Override
1332      public int hashCode()
1333      {
1334        //Taken from the Effective Java book by Joshua Bloch.
1335        //The constants 17 & 37 are arbitrary & carry no meaning.
1336        int result = 17;
1337        
1338        //You MUST keep this method in sync w/ the equals method
1339        result = 37 * result + projectCode.hashCode();
1340        result = 37 * result + Boolean.valueOf(isTest).hashCode();
1341        result = 37 * result + title.hashCode();
1342        result = 37 * result + editor.hashCode();
1343        result = 37 * result + proposalCode.hashCode();
1344        result = 37 * result + projectType.hashCode();
1345        result = 37 * result + getExecutionStatus().hashCode();
1346        result = 37 * result + telescope.hashCode();
1347        result = 37 * result + scientificPriority.hashCode();
1348        result = 37 * result + comments.hashCode();
1349        result = 37 * result + new Double(allocatedTime).hashCode();
1350        result = 37 * result + new Double(timeUsedSoFar).hashCode();
1351    
1352        result = 37 * result + progBlocks.hashCode();
1353    
1354        return result;
1355      }
1356    
1357      //============================================================================
1358      // 
1359      //============================================================================
1360    
1361      //This is here for quick manual testing
1362      /*
1363      public static void main(String[] args)
1364      {
1365        ProjectBuilder builder = new ProjectBuilder();
1366        builder.setIdentifiers(true);
1367        
1368        Project proj = builder.makeProject(null);
1369    
1370        try
1371        {
1372          proj.writeAsXmlTo(new java.io.PrintWriter(System.out));
1373        }
1374        catch (JAXBException ex)
1375        {
1376          System.out.println("Trouble w/ proj.toXml.  Msg:");
1377          System.out.println(ex.getMessage());
1378          ex.printStackTrace();
1379          
1380          System.out.println("Attempting to write XML w/out schema verification:");
1381          JaxbUtility.getSharedInstance().setLookForDefaultSchema(false);
1382          try
1383          {
1384            proj.writeAsXmlTo(new java.io.PrintWriter(System.out));
1385          }
1386          catch (JAXBException ex2)
1387          {
1388            System.out.println("Still had trouble w/ proj.toXml.  Msg:");
1389            System.out.println(ex.getMessage());
1390            ex.printStackTrace();
1391          }
1392        }
1393        try
1394        {
1395          java.io.FileWriter writer =
1396            new java.io.FileWriter("/export/home/calmer/dharland/JUNK/project.xml");
1397          proj.writeAsXmlTo(writer);
1398        }
1399        catch (Exception ex3)
1400        {
1401          System.out.println(ex3.getMessage());
1402          ex3.printStackTrace();
1403        }
1404      }
1405      */
1406      /*
1407      public static void main(String... args) throws Exception
1408      {
1409        java.io.FileReader reader =
1410          new java.io.FileReader("/export/home/calmer/dharland/JUNK/Project.xml");
1411        
1412        Project proj = Project.fromXml(reader);
1413        
1414        java.io.FileWriter writer =
1415          new java.io.FileWriter("/export/home/calmer/dharland/JUNK/Project-OUT.xml");
1416        proj.writeAsXmlTo(writer);
1417      }
1418      */
1419      /*
1420      public static void main(String[] args)
1421      {
1422        Project sender   = new Project();
1423        Project receiver = new Project();
1424        
1425        ProjectBuilder builder = new ProjectBuilder();
1426        
1427        ProgramBlock postReq = builder.makeProgramBlock();
1428        postReq.setLongName("Holder of Prereq");
1429        postReq.setShortName("postReq");
1430    
1431        ProgramBlock preReq = builder.makeProgramBlock();
1432        preReq.setLongName("Prerequisite");
1433        preReq.setShortName("preReq");
1434        
1435        postReq.addPrerequisite(preReq);
1436        sender.addProgramBlock(postReq);
1437        
1438        ProgramBlock postReqClone = postReq.clone();
1439        receiver.addProgramBlock(postReqClone);
1440        
1441        List<ProgramBlock> senderPbs   =   sender.getProgramBlocks();
1442        List<ProgramBlock> receiverPbs = receiver.getProgramBlocks();
1443        
1444        for (int p=0; p < senderPbs.size(); p++)
1445        {
1446          System.out.println("p="+p+"  value-equal? => " + senderPbs.get(p).equals(receiverPbs.get(p)) +
1447                             ", reference-equal? => " + (senderPbs.get(p) == receiverPbs.get(p)));
1448        }
1449        
1450        ProgramBlock receiverPreReq = null;
1451        for (ProgramBlock pb : receiverPbs.get(0).getDirectPrerequisites())
1452        {
1453          receiverPreReq = pb;
1454          break;
1455        }
1456        
1457        if (receiverPreReq == preReq)
1458          System.out.println("Crud, we have prereq PB from another project.");
1459        else
1460          System.out.println("Good, prereq PB is from same project.");
1461      }
1462      */
1463    }