View Javadoc

1   /* -*- mode: JDE; c-basic-offset: 2; indent-tabs-mode: nil -*-
2    *
3    * $Id: FileRelease.java,v 1.14 2003/08/07 21:42:23 ljnelson Exp $
4    *
5    * Copyright (c) 2003 Laird Jarrett Nelson.
6    *
7    * Permission is hereby granted, free of charge, to any person obtaining a copy
8    * of this software and associated documentation files (the "Software"), to deal
9    * in the Software without restriction, including without limitation the rights
10   * to use, copy, modify, merge, publish, distribute, sublicense and/or sell 
11   * copies of the Software, and to permit persons to whom the Software is 
12   * furnished to do so, subject to the following conditions:
13   * 
14   * The above copyright notice and this permission notice shall be included in 
15   * all copies or substantial portions of the Software.
16   * 
17   * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
19   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
20   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
21   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23   * SOFTWARE.
24   *
25   * The original copy of this license is available at
26   * http://www.opensource.org/license/mit-license.html.
27   */
28  package sfutils.frs;
29  
30  import java.io.File;
31  
32  import java.util.Arrays;
33  import java.util.Collection;
34  import java.util.Map;
35  import java.util.LinkedHashMap;
36  import java.util.Date;
37  
38  import sfutils.Project; // for Javadoc only
39  import sfutils.SourceForgeException;
40  
41  /***
42   * A {@link HideableNamedObject} corresponding to a <a
43   * href="http://sourceforge.net/">SourceForge</a> file release, the fundamental
44   * unit of publication at <a href="http://sourceforge.net/">SourceForge</a>.
45   *
46   * <p>Briefly:
47   *
48   * <ul>
49   *
50   * <li>a <a href="http://sourceforge.net/">SourceForge</a> <b>project</b> has
51   *     one or more <b>packages</b></li>
52   *
53   * <li>a <a href="http://sourceforge.net/">SourceForge</a> package has zero or
54   *     more <b>file releases</b></li>
55   *
56   * <li>a <a href="http://sourceforge.net/">SourceForge</a> file release has one
57   *     or more <b>file specifications</b></li>
58   *
59   * </ul>
60   *
61   * These are represented by the following classes, in order:
62   *
63   * <ul>
64   *
65   * <li>{@link Project}</li>
66   *
67   * <li>{@link Package}</li>
68   *
69   * <li>{@link FileRelease}</li>
70   *
71   * <li>{@link FileSpecification}</li>
72   *
73   * </ul></p>
74   *
75   * @author     <a href="mailto:ljnelson94@alumni.amherst.edu">Laird Nelson</a>
76   * @version    $Revision: 1.14 $ $Date: 2003/08/07 21:42:23 $
77   * @since      May 21, 2003 
78   * @see        Project
79   * @see        Package
80   * @see        FileRelease
81   * @see        FileSpecification 
82   * @see        <a
83   *               href="http://sourceforge.net/docman/display_doc.php?docid=6445&group_id=1">Guide
84   *               to the SourceForge.net File Release System (FRS)</a>
85   * @see        <a href="package-summary.html#package_description">The
86   *               <code>sfutils.frs</code> package description</a>
87   */
88  public class FileRelease 
89    extends HideableNamedObject implements FileSpecificationMap {
90  
91    /***
92     * The identifier of this {@link FileRelease}.  {@link FileRelease}
93     * identifiers are assigned by <a 
94     * href="http://sourceforge.net/">SourceForge</a>.  This field may be
95     * <code>null</code>.
96     */
97    private String id;
98  
99    /***
100    * The {@link Package} this {@link FileRelease} is a part of.  This field may
101    * be <code>null</code>.
102    */
103   private Package projectPackage;
104   
105   /***
106    * The {@link Date} on which this {@link FileRelease} is to be released.  This
107    * field may be <code>null</code>.
108    */
109   private Date releaseDate;
110 
111   /***
112    * Whether or not to preserve any formatting that is present in either the
113    * {@link #releaseNotes} or {@link #changeLog} field.  This field is
114    * initialized to <code>true</code>.
115    */
116   private boolean preserveFormattedText;
117 
118   /***
119    * Whether or not to send email to those <a 
120    * href="http://sourceforge.net/">SourceForge</a> users who are monitoring
121    * this {@link FileRelease}'s {@link Package} describing the new release.
122    * This field is initialized to <code>true</code>.  
123    */
124   private boolean notifyOthers;
125 
126   /***
127    * A {@link String} containing the release notes for this {@link FileRelease}.
128    * This field is ignored if the {@link #releaseNotesFile} field is
129    * non-<code>null</code>.
130    *
131    * <p>This field may be <code>null</code>.</p>
132    * 
133    * @see        #releaseNotesFile
134    */
135   private String releaseNotes;
136 
137   /***
138    * A {@link File} containing the release notes for this {@link FileRelease}.
139    * The {@link #releaseNotes} field will be ignored if this field is
140    * non-<code>null</code>.
141    *
142    * @see        #releaseNotes
143    */
144   private File releaseNotesFile;
145 
146   /***
147    * A {@link String} containing the change log for this {@link FileRelease}.
148    * This field is ignored if the {@link #changeLogFile} field is
149    * non-<code>null</code>.
150    *
151    * <p>This field may be <code>null</code>.</p>
152    * 
153    * @see        #changeLogFile
154    */
155   private String changeLog;
156 
157   /***
158    * A {@link File} containing the change log for this {@link FileRelease}.  The
159    * {@link #changeLog} field will be ignored if this field is
160    * non-<code>null</code>.
161    *
162    * @see        #releaseNotes 
163    */
164   private File changeLogFile;
165 
166   /***
167    * A {@link Map} of {@link FileSpecification}s indexed by the {@linkplain
168    * File#getName() unqualified name}s of their {@linkplain
169    * FileSpecification#getFile() associated <code>File</code>}s.  This field
170    * cannot be <code>null</code>.  
171    */
172   private final Map specs;
173 
174   /***
175    * A {@link Publisher} that handles the uploading and distribution of this
176    * {@link FileRelease} to <a href="http://sourceforge.net/">SourceForge</a>.
177    * This field may be <code>null</code>.
178    */
179   private Publisher publisher;
180 
181   /***
182    * Creates a new {@link FileRelease}.  This constructor simply calls the
183    * {@link #FileRelease(Package, String)} constructor with <code>null</code>
184    * arguments.
185    *
186    * <p>The resulting {@link FileRelease} is in an illegal state until, at a
187    * minimum, its {@linkplain #setName(String) associated name} and {@linkplain
188    * #setPackage(Package) associated <code>Package</code>} are set.</p>
189    *
190    * @see        #FileRelease(Package, String)
191    * @see        #setName(String)
192    * @see        #setPackage(Package)
193    */
194   public FileRelease() {
195     this(null, null);
196   }
197 
198   /***
199    * Creates a new {@link FileRelease} and sets its {@linkplain #setName(String)
200    * associated name} to the supplied name.  In addition, this constructor
201    * creates a new {@link Package} and {@linkplain Package#setName(String) sets
202    * its associated name} to the supplied name as well.
203    *
204    * <p>This constructor {@linkplain Package#Package(String) creates a new
205    * <code>Package</code> with the supplied name}, and passes it and the
206    * supplied name to the {@link #FileRelease(Package, String)} constructor.</p>
207    *
208    * @param      name
209    *               the name of this {@link FileRelease} (and the name that will
210    *               be assigned to the new {@link Package} it will belong to);
211    *               may be <code>null</code>, rather uselessly
212    * @see        #FileRelease(Package, String)
213    * @see        Package#Package(String)
214    */
215   public FileRelease(final String name) {
216     this(new Package(name), name);
217   }
218 
219   /***
220    * Creates a new {@link FileRelease} that belongs to the supplied {@link
221    * Package}.  This constructor simply calls the {@link #FileRelease(Package,
222    * String)} constructor, passing it the supplied {@link Package} and a
223    * <code>null</code> name.
224    *
225    * @param      projectPackage
226    *               the {@link Package} to which this {@link FileRelease}
227    *               belongs; may be <code>null</code>, rather uselessly
228    * @see        #FileRelease(Package, String)
229    */
230   public FileRelease(final Package projectPackage) {
231     this(projectPackage, null);
232   }
233 
234   /***
235    * Creates a new {@link FileRelease} with the supplied name that belongs to
236    * the supplied {@link Package}.  In addition, this constructor also
237    * {@linkplain #setReleaseDate(Date) sets the release date} to today's date,
238    * {@linkplain #setPreserveFormattedText(boolean) indicates that preformatted
239    * change log or release notes text is to be preserved}, and {@linkplain
240    * #setNotifyOthers(boolean) sets this <code>FileRelease</code> up to notify
241    * those monitoring it when it is released}.
242    *
243    * @param      projectPackage
244    *               the {@link Package} to which this {@link FileRelease}
245    *               belongs; may be <code>null</code>, rather uselessly
246    * @param      name
247    *               the name of this new {@link FileRelease}; may be
248    *               <code>null</code>, rather uselessly
249    * @see        #setReleaseDate(Date)
250    * @see        #setPreserveFormattedText(boolean)
251    * @see        #setNotifyOthers(boolean) 
252    */
253   public FileRelease(final Package projectPackage,
254                      final String name) {
255     super(name);
256     this.specs = new LinkedHashMap(7);
257     this.setNotifyOthers(true);
258     this.setPackage(projectPackage);
259     this.setPreserveFormattedText(true);
260     this.setReleaseDate(new Date());
261   }
262 
263   /***
264    * Sets the identifier of this {@link FileRelease}.  Identifiers are assigned
265    * by <a href="http://sourceforge.net/">SourceForge</a>.
266    *
267    * @param      id
268    *               the identifier to set; should be a valid <a
269    *               href="http://sourceforge.net/">SourceForge</a>-assigned file
270    *               release identifier, but may be <code>null</code> (rather
271    *               uselessly)
272    */
273   public void setID(final String id) {
274     this.id = id;
275   }
276 
277   /***
278    * Returns the identifier of this {@link FileRelease}.  This method may return
279    * <code>null</code>.
280    *
281    * @return     the identifier of this {@link FileRelease}, or
282    *               <code>null</code>
283    */
284   public String getID() {
285     return this.id;
286   }
287 
288   /***
289    * Returns whether or not those monitoring this {@link FileRelease} will be
290    * notified when it is published.
291    *
292    * @return     <code>true</code> if those monitoring this {@link FileRelease}
293    *               will be notified when it is published; <code>false</code>
294    *               otherwise
295    */
296   public boolean getNotifyOthers() {
297     return this.notifyOthers;
298   }
299   
300   /***
301    * Sets whether or not those monitoring this {@link FileRelease} will be
302    * notified when it is published.
303    *
304    * @param      notify
305    *               if <code>true</code>, those monitoring this {@link 
306    *               FileRelease} will be notified when it is published
307    */
308   public void setNotifyOthers(final boolean notify) {
309     this.notifyOthers = notify;
310   }
311 
312   /***
313    * Returns whether or not text present in the {@linkplain #getReleaseNotes()
314    * release notes} or {@linkplain #getChangeLog() change log} will have its
315    * formatting preserved.
316    *
317    * @return     <code>true</code> if text present in the {@linkplain 
318    *               #getReleaseNotes() release notes} or {@linkplain
319    *               #getChangeLog() change log} will have its formatting
320    *               preserved; <code>false</code> otherwise 
321    */
322   public boolean getPreserveFormattedText() {
323     return this.preserveFormattedText;
324   }
325 
326   /***
327    * Sets whether or not text present in the {@linkplain #getReleaseNotes()
328    * release notes} or {@linkplain #getChangeLog() change log} will have its
329    * formatting preserved.
330    *
331    * @param      preserve
332    *               if <code>true</code>, text present in the {@linkplain
333    *               #getReleaseNotes() release notes} or {@linkplain
334    *               #getChangeLog() change log} will have its formatting
335    *               preserved 
336    */
337   public void setPreserveFormattedText(final boolean preserve) {
338     this.preserveFormattedText = preserve;
339   }
340 
341   /***
342    * Returns the {@link Package} to which this {@link FileRelease} belongs.
343    * This method may return <code>null</code>.
344    *
345    * @return     the {@link Package} to which this {@link FileRelease} belongs,
346    *               or <code>null</code>
347    */
348   public Package getPackage() {
349     return this.projectPackage;
350   }
351   
352   /***
353    * Sets the {@link Package} to which this {@link FileRelease} belongs.
354    *
355    * @param      projectPackage
356    *               the {@link Package} to which this {@link FileRelease}
357    *               belongs; may be <code>null</code> (rather uselessly)
358    */
359   public void setPackage(final Package projectPackage) {
360     this.projectPackage = projectPackage;
361   }
362 
363   /***
364    * Returns the {@link Date} on which this {@link FileRelease} is to be
365    * considered released.  The returned {@link Date} may be <code>null</code>,
366    * and may not correspond to the actual date on which this {@link FileRelease}
367    * is released.
368    *
369    * @return     the {@link Date} on which this {@link FileRelease} is to be
370    *               considered released, or <code>null</code>
371    */
372   public Date getReleaseDate() {
373     return this.releaseDate;
374   }
375 
376   /***
377    * Sets the {@link Date} on which this {@link FileRelease} is to be considered
378    * released.
379    *
380    * @param      date
381    *               the {@link Date} on which this {@link FileRelease} is to be
382    *               considered released; may be <code>null</code>
383    */
384   public void setReleaseDate(final Date date) {
385     this.releaseDate = date;
386   }
387 
388   /***
389    * Returns the release notes for this {@link FileRelease}.  If this method
390    * returns <code>null</code>, make sure to check the return value of the
391    * {@link #getReleaseNotesFile()} method as well.
392    *
393    * @return     the release notes for this {@link FileRelease}, or
394    *               <code>null</code>
395    * @see        #getReleaseNotesFile()
396    */
397   public String getReleaseNotes() {
398     return this.releaseNotes;
399   }
400 
401   /***
402    * Sets the release notes for this {@link FileRelease}.  Note that the
403    * contents of a {@linkplain #getReleaseNotesFile() release notes
404    * <code>File</code>} will be used in place of {@linkplain #getReleaseNotes()
405    * ad-hoc release notes} wherever possible.
406    *
407    * @param      releaseNotes
408    *               the release notes for this {@link FileRelease} ; may be
409    *               <code>null</code> and will be ignored if the return value
410    *               of the {@link #getReleaseNotesFile()} method is
411    *               non-<code>null</code>
412    * @see        #getReleaseNotesFile()
413    * @see        #setReleaseNotesFile(File)
414    */
415   public void setReleaseNotes(final String releaseNotes) 
416     throws IllegalArgumentException {
417     if (releaseNotes != null) {
418       final long lengthInBytes = releaseNotes.length() * 2;
419       if (lengthInBytes < 20L || lengthInBytes > 256000L) {
420         throw new IllegalArgumentException("SourceForge requires that release " +
421                                            "notes be between 20 and 256,000 " +
422                                            "bytes in length");
423       }
424     }
425     this.releaseNotes = releaseNotes;
426   }
427 
428   /***
429    * Returns the {@link File} that contains the release notes for this {@link
430    * FileRelease}.  This method may return <code>null</code>.  If this method
431    * does <i>not</i> return <code>null</code>, then the returned {@link File}
432    * is guaranteed to be {@linkplain File#canRead() readable}.
433    *
434    * @return     a {@linkplain File#canRead() readable} {@link File} that
435    *               contains the release notes for this {@link FileRelease}, or
436    *               <code>null</code> 
437    */
438   public File getReleaseNotesFile() {
439     return this.releaseNotesFile;
440   }
441 
442   /***
443    * Sets the {@link File} that contains release notes for this {@link
444    * FileRelease}.  If the supplied {@link File} is non-<code>null</code>, then
445    * it must {@linkplain File#canRead() exist and be readable}.
446    *
447    * @param      releaseNotesFile
448    *               the {@link File} that contains release notes; may be
449    *               <code>null</code>, but if <i>non</i>-<code>null</code>
450    *               {@linkplain File#canRead() must exist and be readable}
451    * @exception  IllegalArgumentException
452    *               if the supplied {@link File} is not {@linkplain
453    *               File#canRead() readable}
454    */
455   public void setReleaseNotesFile(final File releaseNotesFile) 
456     throws IllegalArgumentException {
457     if (releaseNotesFile != null) {
458       if (!releaseNotesFile.canRead()) {
459         throw new IllegalArgumentException("release notes file must be readable");
460       } else {
461         final long length = releaseNotesFile.length();
462         if (length < 20L || length > 256000L) {
463           throw new IllegalArgumentException("SourceForge requires that release " +
464                                              "notes be between 20 and 256,000 " +
465                                              "bytes in length");
466         }
467       }
468     }
469     this.releaseNotesFile = releaseNotesFile;
470   }
471 
472   /***
473    * Returns the change log for this {@link FileRelease}.  If this method
474    * returns <code>null</code>, make sure to check the return value of the
475    * {@link #getChangeLogFile()} method as well.
476    *
477    * @return     the change log for this {@link FileRelease}, or 
478    *               <code>null</code>
479    * @see        #getChangeLogFile()
480    */
481   public String getChangeLog() {
482     return this.changeLog;
483   }
484 
485   /***
486    * Sets the change log for this {@link FileRelease}.  Note that the contents
487    * of a {@linkplain #getChangeLogFile() change log <code>File</code>} will be
488    * used in place of {@linkplain #getChangeLog() ad-hoc change log} wherever
489    * possible.
490    *
491    * @param      changeLog
492    *               the change log for this {@link FileRelease} ; may be
493    *               <code>null</code> and will be ignored if the return value of
494    *               the {@link #getChangeLogFile()} method is
495    *               non-<code>null</code>
496    * @see        #getChangeLogFile()
497    * @see        #setChangeLogFile(File) 
498    */
499   public void setChangeLog(final String changeLog) {
500     if (changeLog != null) {
501       final long lengthInBytes = changeLog.length() * 2;
502       if (lengthInBytes < 20L || lengthInBytes > 256000L) {
503         throw new IllegalArgumentException("SourceForge requires that " +
504                                            "changelog be between 20 and " +
505                                            "256,000 bytes in length");
506       }
507     }
508     this.changeLog = changeLog;
509   }
510 
511   /***
512    * Returns the {@link File} that contains the change log for this {@link
513    * FileRelease}.  This method may return <code>null</code>.  If this method
514    * does <i>not</i> return <code>null</code>, then the returned {@link File}
515    * is guaranteed to be {@linkplain File#canRead() readable}.
516    *
517    * @return     a {@linkplain File#canRead() readable} {@link File} that
518    *               contains the change log for this {@link FileRelease}, or
519    *               <code>null</code> 
520    */
521   public File getChangeLogFile() {
522     return this.changeLogFile;
523   }
524 
525   /***
526    * Sets the {@link File} that contains the change log for this {@link
527    * FileRelease}.  If the supplied {@link File} is non-<code>null</code>, then
528    * it must {@linkplain File#canRead() exist and be readable}.
529    *
530    * @param      changeLogFile
531    *               the {@link File} that contains the change log; may be
532    *               <code>null</code>, but if <i>non</i>-<code>null</code>
533    *               {@linkplain File#canRead() must exist and be readable}
534    * @exception  IllegalArgumentException
535    *               if the supplied {@link File} is not {@linkplain
536    *               File#canRead() readable} 
537    */
538   public void setChangeLogFile(final File changeLogFile) 
539     throws IllegalArgumentException {
540     if (changeLogFile != null) {
541       if (!changeLogFile.canRead()) {
542         throw new IllegalArgumentException("change log file must be readable");
543       } else {
544         final long length = changeLogFile.length();
545         if (length < 20L || length > 256000L) {
546           throw new IllegalArgumentException("SourceForge requires that " +
547                                              "changelogs  be between 20 and " +
548                                              "256,000 bytes in length");
549         }
550       }
551     }
552     this.changeLogFile = changeLogFile;
553   }
554 
555   /***
556    * A convenience method that returns all {@link File}s contained by the {@link
557    * #getFileSpecifications() FileSpecification}s associated with this {@link
558    * FileRelease}.  This method's implementation iterates through this {@link
559    * FileRelease}'s {@linkplain #getFileSpecifications() associated
560    * <code>FileSpecification</code>s} and extracts their associated {@link
561    * File}s.  This method may return <code>null</code>.  Additionally, it cannot
562    * be guaranteed that there are not <code>null</code> elements inside the
563    * returned {@link File} array.
564    *
565    * @return     all {@link File}s indirectly associated with this {@link
566    *               FileRelease}, or <code>null</code>
567    */
568   public final File[] getFiles() {
569     final FileSpecification[] specs = this.getFileSpecifications();
570     if (specs == null) {
571       return null;
572     }
573     final File[] files = new File[specs.length];
574     FileSpecification spec;
575     for (int i = 0; i < specs.length; i++) {
576       spec = specs[i];
577       if (spec == null) {
578         files[i] = null;
579       } else {
580         files[i] = spec.getFile();
581       }
582     }
583     return files;
584   }
585 
586   /***
587    * A convenience method that returns all {@linkplain File#getName() filenames}
588    * indirectly associated with this {@link FileRelease}.  This method's
589    * implementation iterates through this {@link FileRelease}'s {@linkplain
590    * #getFiles() associated <code>File</code>s} and extract their {@linkplain
591    * File#getName() names}.  This method may return <code>null</code>.
592    * Additionally, it cannot be guaranteed that there are not <code>null</code>
593    * elements inside the returned {@link File} array.
594    *
595    * @return     all {@linkplain File#getName() filenames} indirectly associated
596    *               with this {@link FileRelease}, or <code>null</code>
597    */
598   public final String[] getShortFileNames() {
599     final File[] files = this.getFiles();
600     if (files == null) {
601       return null;
602     }
603     final String[] names = new String[files.length];
604     File file;
605     for (int i = 0; i < files.length; i++) {
606       file = files[i];
607       if (file == null) {
608         names[i] = null;
609       } else {
610         names[i] = file.getName();
611       }
612     }
613     return names;
614   }
615 
616   /***
617    * Returns all the {@link FileSpecification}s associated with this {@link
618    * FileRelease}.  This method may return <code>null</code>.
619    *
620    * @return     all the {@link FileSpecification}s associated with this {@link
621    *               FileRelease}, or <code>null</code>
622    */
623   public FileSpecification[] getFileSpecifications() {
624     final Collection values = this.specs.values();
625     if (values == null) {
626       return null;
627     }
628     return (FileSpecification[])values.toArray(new FileSpecification[values.size()]);
629   }
630 
631   /***
632    * Returns the {@link FileSpecification} that contains the {@link File} with
633    * the supplied {@linkplain File#getName() name}, or <code>null</code> if no
634    * such {@link FileSpecification} exists.
635    *
636    * @param      fileBaseName
637    *               the {@linkplain File#getName() short name} of the {@link
638    *               File} in question; may be <code>null</code>
639    * @return     the {@link FileSpecification} that contains the {@link File}
640    *               with the supplied {@linkplain File#getName() name}, or
641    *               <code>null</code> 
642    */
643   public FileSpecification getFileSpecification(final String fileBaseName) {
644     return (FileSpecification)this.specs.get(fileBaseName);
645   }
646 
647   /***
648    * Sets the {@link FileSpecification}s that are to be associated with this
649    * {@link FileRelease}.
650    *
651    * <p>If the supplied {@link FileSpecification} array is
652    * non-<code>null</code>, then no element of it may be <code>null</code> or an
653    * {@link IllegalArgumentException} will be thrown.  In addition, any
654    * non-<code>null</code> {@link FileSpecification#getFile() File} associated
655    * with any given {@link FileSpecification} in the array must {@linkplain
656    * File#canRead() exist and be readable}.</p>
657    *
658    * @param      specs
659    *               the {@link FileSpecification}s; may be <code>null</code>
660    * @exception  IllegalArgumentException
661    *               if any of the {@link FileSpecification}s in the array is not
662    *               set up properly; see this method's description for details 
663    */
664   public void setFileSpecifications(final FileSpecification[] specs) 
665     throws IllegalArgumentException {
666     if (specs != null) {
667       FileSpecification spec;
668       File file;
669       String name;
670       for (int i = 0; i < specs.length; i++) {
671         spec = specs[i];
672         if (spec == null) {
673           throw new IllegalArgumentException("specs contains null elements");
674         }
675         file = spec.getFile();
676         FileSpecification.validate(file);
677         name = file.getName();
678         assert name != null;
679         this.specs.put(name, spec);
680       }
681     }
682   }
683 
684   /***
685    * Returns the {@link Publisher} used to upload this {@link FileRelease} to <a
686    * href="http://sourceforge.net/">SourceForge</a>.  This method may return
687    * <code>null</code>.
688    *
689    * @return     the {@link Publisher} used to upload this {@link FileRelease}
690    *               to <a href="http://sourceforge.net/">SourceForge</a>, or
691    *               <code>null</code> 
692    */
693   public Publisher getPublisher() {
694     return this.publisher;
695   }
696 
697   /***
698    * Installs the supplied {@link Publisher} as the {@link Publisher} that will
699    * be used to upload this {@link FileRelease} to <a
700    * href="http://sourceforge.net/">SourceForge</a>.
701    *
702    * @param      publisher
703    *               the new {@link Publisher}; may be <code>null</code>
704    */
705   public void setPublisher(final Publisher publisher) {
706     this.publisher = publisher;
707   }
708 
709   /***
710    * Publishes this {@link FileRelease} to <a
711    * href="http://sourceforge.net/">SourceForge</a>.  A non-<code>null</code>
712    * {@link Publisher} must {@linkplain #setPublisher(Publisher) have been
713    * previously installed}.
714    *
715    * @exception  SourceForgeException
716    *               if an error occurs
717    * @see        Publisher#publish(FileRelease)
718    */
719   public void publish() throws SourceForgeException {
720     final Publisher publisher = this.getPublisher();
721     if (publisher == null) {
722       throw new PublishingException("Call setPublisher() first");
723     }
724     publisher.publish(this);
725   }
726 
727   /***
728    * Returns a {@link String} representation of this {@link FileRelease}.  This
729    * method never returns <code>null</code>.
730    *
731    * @return     a {@link String} representation of this {@link FileRelease};
732    *               never <code>null</code>
733    */
734   public String toString() {
735     final StringBuffer returnMe = new StringBuffer();
736     final String superRep = super.toString();
737     if (superRep != null) {
738       returnMe.append(superRep);
739     } else {
740       final String name = this.getName();
741       if (name != null) {
742         returnMe.append(name);
743       }
744     }
745     if (returnMe.length() > 0) {
746       returnMe.append(" ");
747     }
748     final FileSpecification[] specs = this.getFileSpecifications();
749     if (specs != null) {
750       returnMe.append(String.valueOf(Arrays.asList(specs)));
751     }
752     return returnMe.toString();
753   }
754 
755 }