View Javadoc

1   /* -*- mode: JDE; c-basic-offset: 2; indent-tabs-mode: nil -*-
2    *
3    * $Id: FileReleaseSystem.java,v 1.21 2004/07/27 20:20:35 ljnelson Exp $
4    *
5    * Copyright (c) 2003, 2004 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.BufferedInputStream;
31  import java.io.BufferedOutputStream;
32  import java.io.File;
33  import java.io.FileInputStream;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.io.OutputStream;
37  import java.io.UnsupportedEncodingException;
38  
39  import java.net.URL;
40  import java.net.URLConnection;
41  import java.net.URLEncoder;
42  
43  import java.text.DateFormat;
44  import java.text.SimpleDateFormat;
45  
46  import java.util.Arrays;
47  import java.util.Collections;
48  import java.util.Comparator;
49  import java.util.Date;
50  import java.util.Iterator;
51  import java.util.List;
52  import java.util.SortedSet;
53  import java.util.TreeSet;
54  
55  import java.util.logging.Logger;
56  
57  import java.util.regex.Matcher;
58  import java.util.regex.Pattern;
59  
60  import com.meterware.httpunit.GetMethodWebRequest;
61  import com.meterware.httpunit.TableCell;
62  import com.meterware.httpunit.UploadFileSpec;
63  import com.meterware.httpunit.WebConversation;
64  import com.meterware.httpunit.WebForm;
65  import com.meterware.httpunit.WebLink;
66  import com.meterware.httpunit.WebResponse;
67  import com.meterware.httpunit.WebTable;
68  
69  import org.xml.sax.SAXException;
70  
71  import sfutils.Administrator;
72  import sfutils.InvalidCredentialsException;
73  import sfutils.InvalidProjectIDException;
74  import sfutils.NotLoggedInException;
75  import sfutils.PermissionDeniedException;
76  import sfutils.Project;
77  import sfutils.SourceForgeClient;
78  import sfutils.SourceForgeException;
79  import sfutils.SourceForgeUIChangeException;
80  
81  public class FileReleaseSystem extends SourceForgeClient implements Publisher {
82  
83    // TODO: work on the editFiles() method.
84  
85    // IDEA: what if we figure out whether a file release already has files by actually hitting the file release interface, i.e. the non-admin one?  If we 
86  
87    private static final Logger LOGGER;
88  
89    private static final DateFormat DATE_FORMATTER;
90  
91    private static final Comparator REVERSE_COMPARATOR;
92    
93    private static final Object PATTERN_LOCK;
94  
95    private static final Pattern RELEASE_ID_PATTERN;
96  
97    static {
98      LOGGER = Logger.getLogger(FileReleaseSystem.class.getName());
99      assert LOGGER != null;
100     DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd");
101     REVERSE_COMPARATOR = new ReverseComparator();
102     PATTERN_LOCK = new byte[0];
103     RELEASE_ID_PATTERN = Pattern.compile("release_id=(//d+)");
104     assert RELEASE_ID_PATTERN != null;
105   }
106 
107   private volatile int uploadThreadsOutstanding;
108 
109   private Exception uploadException;
110 
111   private FileUploadListener[] listeners;
112 
113   public FileReleaseSystem() {
114     super();
115   }
116   
117   public FileReleaseSystem(final String user,
118                            final String password) 
119     throws InvalidCredentialsException, SourceForgeException {
120     super(user, password);
121   }
122 
123   /*
124    * FileRelease methods
125    */
126 
127   /***
128    * Publishes the supplied {@link FileRelease} to its associated {@link
129    * Project} area on <a href="http://sourceforge.net/">SourceForge</a>.
130    *
131    * @param      release
132    *               the {@link FileRelease} to publish; must not be
133    *               <code>null</code>
134    * @exception  SourceForgeException
135    *               if the supplied {@link FileRelease} could not be published
136    */
137   public void publish(final FileRelease release)
138     throws InvalidProjectIDException,
139            InvalidPackageIDException,
140            InvalidFileReleaseIDException,
141            TooManyPackagesException,
142            TooManyFileReleasesException,
143            PermissionDeniedException,
144            NotLoggedInException,
145            SourceForgeUIChangeException,
146            SourceForgeException {
147     assertNotNull(release, "release");
148     if (!this.isLoggedIn()) {
149       this.login(release);
150     }
151 
152     // First, upload all the files asynchronously.
153     final File[] files = release.getFiles();
154     final boolean hasFiles = files != null && files.length > 0;
155     FileUploadListener notifier = null;
156     try {
157       if (hasFiles) {
158         notifier = 
159           new FileUploadListener() {
160               public void fileUploaded(final FileUploadEvent event) {
161                 synchronized (FileReleaseSystem.this) {
162                   --uploadThreadsOutstanding;
163                   FileReleaseSystem.this.notify();
164                 }
165               }
166               
167               public void exceptionCaught(final FileUploadEvent event) {
168                 synchronized (FileReleaseSystem.this) {        
169                   if (event != null && uploadException == null) {
170                     uploadException = event.getException();
171                   }
172                   --uploadThreadsOutstanding;
173                   FileReleaseSystem.this.notify();
174                 }
175               }
176             };
177         this.addFileUploadListener(notifier);
178         File file;
179         for (int i = 0; i < files.length; i++) {
180           file = files[i];
181           if (file != null) {
182             synchronized (this) {
183               ++this.uploadThreadsOutstanding;
184               this.uploadFileAsynchronously(file);
185             }
186           }
187         }
188       }
189 
190       // After uploading all the files, go to the page that lets you specify
191       // details about the new (and therefore existing) or old (and therefore
192       // also existing) file release.
193       this.editExistingFileRelease(release);
194       if (hasFiles) {
195 
196         // If there were files that were uploaded, then wait to be notified by
197         // the uploading threads.  When there are no more upload threads
198         // running, the uploading process has finished.
199         //
200         // 
201         synchronized (this) {
202           while (this.uploadThreadsOutstanding > 0) {
203             this.ensureNoUploadErrors();
204             try {
205               this.wait(5 * 60 * 1000);
206             } catch (final InterruptedException throwMe) {
207               throw new SourceForgeException(throwMe);
208             }
209           }
210           this.ensureNoUploadErrors();
211         }
212 
213         // Now that the upload has completed, we can add the newly uploaded
214         // files to the file release.
215         this.addFiles(release);
216       }
217       if (release.getNotifyOthers()) {
218         this.notifyOthers(release);
219       }
220     } finally {
221       if (notifier != null) {
222         this.removeFileUploadListener(notifier);
223       }
224     }
225   }
226 
227   public void editExistingFileRelease(final FileRelease release)
228     throws InvalidCredentialsException,
229            InvalidProjectIDException,
230            InvalidPackageIDException,
231            InvalidPackageNameException,
232            InvalidFileReleaseIDException,
233            TooManyPackagesException,
234            TooManyFileReleasesException,
235            NotLoggedInException,
236            PermissionDeniedException,
237            SourceForgeUIChangeException,
238            SourceForgeException {
239     assertNotNull(release, "release");
240     this.editExistingFileRelease(this.getProjectID(release),
241                                  this.getPackageID(release),
242                                  this.getFileReleaseID(release),
243                                  release.getReleaseDate(),
244                                  release.isHidden(),
245                                  release.getPreserveFormattedText(),
246                                  release.getChangeLogFile(),
247                                  release.getChangeLog(),
248                                  release.getReleaseNotesFile(),
249                                  release.getReleaseNotes());
250   }
251 
252   public void editExistingFileRelease(final int projectID,
253                                       final int packageID,
254                                       final int releaseID,
255                                       final Date releaseDate,
256                                       final boolean hidden,
257                                       final boolean preserveFormattedText,
258                                       final File changeLogFile,
259                                       final String changeLogText,
260                                       final File releaseNotesFile,
261                                       final String releaseNotesText) 
262     throws InvalidProjectIDException,
263            InvalidPackageIDException,
264            InvalidFileReleaseIDException,
265            NotLoggedInException,
266            SourceForgeUIChangeException,
267            SourceForgeException {
268     final WebForm editExistingReleaseForm = 
269       this.getEditExistingReleaseForm(projectID, packageID, releaseID);
270     if (editExistingReleaseForm == null) {
271       throw new SourceForgeUIChangeException();
272     }
273     if (releaseDate != null) {
274       final String formattedDate;
275       synchronized (DATE_FORMATTER) {
276         formattedDate = DATE_FORMATTER.format(releaseDate);
277       }
278       if (formattedDate != null) {
279         editExistingReleaseForm.setParameter("release_date", formattedDate);
280       }
281     }
282     editExistingReleaseForm.setParameter("status_id", hidden ? "3" : "1");
283     if (preserveFormattedText) {
284       editExistingReleaseForm.setParameter("preformatted", "1");
285     } else {
286       editExistingReleaseForm.removeParameter("preformatted");
287     }
288     if (changeLogFile != null && changeLogFile.canRead()) {
289       editExistingReleaseForm.setParameter("uploaded_changes",
290                                            new UploadFileSpec[] { 
291                                              new UploadFileSpec(changeLogFile) 
292                                            });
293     } else if (changeLogText != null) {
294       editExistingReleaseForm.setParameter("release_changes", changeLogText);
295     }
296     if (releaseNotesFile != null && releaseNotesFile.canRead()) {
297       editExistingReleaseForm.setParameter("uploaded_notes",
298                                            new UploadFileSpec[] { 
299                                              new UploadFileSpec(releaseNotesFile)
300                                            });
301     } else if (releaseNotesText != null) {
302       editExistingReleaseForm.setParameter("release_notes", releaseNotesText);
303     }
304     try {
305       editExistingReleaseForm.submit();
306     } catch (final SAXException kaboom) {
307       throw new SourceForgeException(kaboom);
308     } catch (final IOException kaboom) {
309       throw new SourceForgeException(kaboom);
310     }
311   }
312 
313   public void addFiles(final FileRelease release)
314     throws NotLoggedInException,
315            InvalidPackageNameException,
316            SourceForgeUIChangeException,
317            SourceForgeException {    
318     this.addFiles(this.getProjectID(release),
319                   this.getPackageID(release),
320                   this.getFileReleaseID(release),
321                   extractShortFileNames(release));
322   }
323 
324   public void addFiles(final int projectID,
325                        final int packageID,
326                        final int releaseID,
327                        final String[] shortFileNames)
328     throws NotLoggedInException,
329            SourceForgeUIChangeException,
330            SourceForgeException {
331     this.addFiles(projectID, 
332                   packageID,
333                   releaseID,
334                   new TreeSet(Arrays.asList(shortFileNames)));
335   }
336 
337   /***
338    * Adds files that have been previously uploaded to the SourceForge file
339    * release identified by the supplied release identifier.  Note that this
340    * method does not <i>configure</i> the files so added.  See the {@link
341    * #editFiles(FileRelease)} method for details.
342    */
343   public void addFiles(final int projectID,
344                        final int packageID,
345                        final int releaseID,
346                        final SortedSet shortFileNamesSet) 
347     throws NotLoggedInException,
348            SourceForgeUIChangeException,
349            SourceForgeException {
350     if (shortFileNamesSet == null || 
351         shortFileNamesSet.isEmpty()) {
352       return;
353     }
354     final WebForm addFilesForm = 
355       this.getAddFilesForm(projectID, packageID, releaseID);
356     if (addFilesForm == null) {
357       throw new SourceForgeUIChangeException();
358     }
359     addFilesForm.setParameter("file_list[]",
360                               (String[])shortFileNamesSet.toArray(new String[shortFileNamesSet.size()]));
361     try {
362       addFilesForm.submit();
363     } catch (final IOException kaboom) {
364       throw new SourceForgeException(kaboom);
365     } catch (final SAXException kaboom) {
366       throw new SourceForgeException(kaboom);
367     }
368   }
369 
370   public void editFiles(final FileRelease release)
371     throws NotLoggedInException,
372            InvalidPackageNameException,
373            SourceForgeUIChangeException,
374            SourceForgeException {
375     this.editFiles(this.getProjectID(release),
376                    this.getPackageID(release),
377                    this.getFileReleaseID(release),
378                    release);
379   }
380 
381   public void editFiles(final int projectID,
382                         final int packageID,
383                         final int releaseID,
384                         final FileSpecificationMap map)
385     throws NotLoggedInException,
386            InvalidProjectIDException,
387            InvalidPackageIDException,
388            InvalidFileReleaseIDException,
389            PermissionDeniedException,
390            SourceForgeUIChangeException,
391            SourceForgeException {
392     if (map == null) {
393       return;
394     }
395     WebResponse editReleasePage = 
396       this.getEditReleasePage(projectID, packageID, releaseID);
397     if (editReleasePage == null) {
398       throw new SourceForgeUIChangeException();
399     }
400     WebForm[] forms = null;
401     try {
402       forms =
403         this.retainFormsWithAction(editReleasePage.getForms(),
404                                    "/project/admin/editreleases.php");
405     } catch (final SAXException kaboom) {
406       throw new SourceForgeException(kaboom);
407     }
408     if (forms == null || forms.length < 2) {
409       throw new SourceForgeUIChangeException();
410     } else if (forms.length == 2) {
411       // No step three form; no files were uploaded.  We're done.
412       return;
413     }
414     // OK, some more weird assembly here.  Get the table that wraps the forms
415     // for editing each individual file.  We have to get this in addition to
416     // the forms themselves, because the file name to which a given form
417     // applies is stored as plain text in a table cell.
418     WebTable table = null;
419     try {
420       table = editReleasePage.getTableStartingWithPrefix("Filename");
421     } catch (final SAXException kaboom) {
422       throw new SourceForgeException(kaboom);
423     }
424     if (table == null) {
425       throw new SourceForgeUIChangeException();
426     }
427     WebForm form;
428     String title;
429     FileSpecification spec;
430     int formIndex = 2;
431     int tableRowIndex = 1;
432     while (formIndex < forms.length) {
433       form = forms[formIndex];
434       assert form != null;
435       if (form.hasParameterNamed("im_sure")) {
436         tableRowIndex += 3;
437       } else if (form.hasParameterNamed("processor_id") &&
438                  form.hasParameterNamed("type_id")) {
439         title = table.getCellAsText(tableRowIndex, 0);
440         assert title != null;
441         spec = map.getFileSpecification(title);
442         assertNotNull(spec, "spec");
443         form.setParameter("processor_id", 
444                           String.valueOf(spec.getProcessorType()));
445         form.setParameter("type_id", Integer.toString(spec.getFileType()));
446         try {
447           editReleasePage = form.submit();
448         } catch (final IOException kaboom) {
449           throw new SourceForgeException(kaboom);
450         } catch (final SAXException kaboom) {
451           throw new SourceForgeException(kaboom);
452         }
453         assert editReleasePage != null;
454         if (formIndex + 1 < forms.length) {
455           try {
456             forms =
457               this.retainFormsWithAction(editReleasePage.getForms(),
458                                          "/project/admin/editreleases.php");
459           } catch (final SAXException kaboom) {
460             throw new SourceForgeException(kaboom);
461           }
462           if (forms == null || forms.length < 2) {
463             throw new SourceForgeUIChangeException();
464           } else if (forms.length == 2) {
465             // No step three form; no files were uploaded.
466             return;
467           }
468           try {
469             table = editReleasePage.getTableStartingWithPrefix("Filename");
470           } catch (final SAXException kaboom) {
471             throw new SourceForgeException(kaboom);
472           }
473           if (table == null) {
474             throw new SourceForgeUIChangeException();
475           }
476         }
477       }
478       formIndex++;
479     }
480   }
481 
482   public void notifyOthers(final FileRelease release)
483     throws NotLoggedInException,
484            InvalidPackageNameException,
485            SourceForgeUIChangeException, 
486            SourceForgeException {
487     this.notifyOthers(this.getProjectID(release),
488                       this.getPackageID(release),
489                       this.getFileReleaseID(release));
490   }
491 
492   public void notifyOthers(final int projectID,
493                            final int packageID,
494                            final int releaseID)
495     throws SourceForgeUIChangeException, 
496            SourceForgeException {
497     final WebForm form =
498       this.getNotifyOthersForm(projectID, packageID, releaseID);
499     if (form == null) {
500       throw new SourceForgeUIChangeException();
501     }
502     try {
503       form.submit();
504     } catch (final SAXException kaboom) {
505       throw new SourceForgeException(kaboom);
506     } catch (final IOException kaboom) {
507       throw new SourceForgeException(kaboom);
508     }
509   }
510 
511   /*
512    * Event methods
513    */
514   public synchronized void addFileUploadListener(final FileUploadListener listener) {
515     if (listener != null) {
516       if (this.listeners == null || 
517           this.listeners.length <= 0) {
518         this.listeners = new FileUploadListener[] { listener };
519       } else {
520         final List list = Arrays.asList(this.listeners);
521         assert list != null;
522         if (!list.contains(listener)) {
523           list.add(listener);
524         }
525         this.listeners =
526           (FileUploadListener[])list.toArray(new FileUploadListener[list.size()]);
527       }
528     }
529   }
530 
531   public synchronized void removeFileUploadListener(final FileUploadListener listener) {
532     if (listener != null && 
533         this.listeners != null && 
534         this.listeners.length > 0) {
535       final List list = Arrays.asList(this.listeners);
536       list.remove(listener);
537       this.listeners =
538         (FileUploadListener[])list.toArray(new FileUploadListener[list.size()]);
539     }
540   }
541 
542   protected final void fireFileUploadedEvent(final File file) {
543     final FileUploadListener[] listeners;
544     synchronized (FileReleaseSystem.class) {
545       if (this.listeners == null) {
546         listeners = null;
547       } else {
548         listeners = (FileUploadListener[])this.listeners.clone();
549       }
550     }
551     if (listeners != null && listeners.length > 0) {
552       final FileUploadEvent event = new FileUploadEvent(this, file, null);
553       FileUploadListener listener;
554       for (int i = 0; i < listeners.length; i++) {
555         listener = listeners[i];
556         if (listener != null) {
557           listener.fileUploaded(event);
558         }
559       }
560     }
561   }
562 
563   protected final void fireExceptionCaughtEvent(final File file,
564                                                 final Exception exception) {
565     final FileUploadListener[] listeners;
566     synchronized (FileReleaseSystem.class) {
567       if (this.listeners == null) {
568         listeners = null;
569       } else {
570         listeners = (FileUploadListener[])this.listeners.clone();
571       }
572     }
573     if (listeners != null && listeners.length > 0) {
574       final FileUploadEvent event = new FileUploadEvent(this, file, exception);
575       FileUploadListener listener;
576       for (int i = 0; i < listeners.length; i++) {
577         listener = listeners[i];
578         if (listener != null) {
579           listener.exceptionCaught(event);
580         }
581       }
582     }
583   }
584 
585   /*
586    * Validation methods
587    */
588 
589   privateg> static final void validatePackageName(String packageName)
590     throws InvalidPackageNameException {
591     ifong> (packageName == null) {
592       throw new InvalidPackageNameException(new NullPointerException("packageName == null"),
593                                             "Package names cannot be null");
594     }
595     packageName = packageName.trim();
596     assert packageName != null;
597     if (packageName.length() <= 0) {
598       throw new InvalidPackageNameException("Package names cannot consist " +
599                                             "solely of whitespace");
600     }
601   }
602 
603   private static final void validateFileReleaseName(String releaseName)
604     throws InvalidFileReleaseNameException {
605     if (releaseName == null) {
606       throw new InvalidFileReleaseNameException(new NullPointerException("releaseName == null"),
607                                                 "Release names cannot be null");
608     }
609     releaseName = releaseName.trim();
610     if (releaseName == null || releaseName.length() <= 0) {
611       throw new InvalidFileReleaseNameException("Release names cannot consist " +
612                                                 "solely of whitespace");
613     }
614   }
615   
616   private static final void validateProjectID(final int projectID)
617     throws InvalidProjectIDException {      
618     if (projectID < 1) {
619       throw new InvalidProjectIDException(String.valueOf(projectID));
620     }
621   }
622 
623   privateg> static final void validatePackageID(final int packageID)
624     throws InvalidPackageIDException {
625     if (packageID < 1) {
626       throwInvalidPackageIDException(String/valueOf(packageID))/package-summary.html">trong> new InvalidPackageIDException(String.valueOf(packageID));
627     }
628   }
629 
630   private static final void validateFileReleaseID(final int releaseID)
631     throws InvalidFileReleaseIDException {
632     if (releaseID < 1) {
633       throw new InvalidFileReleaseIDException(String.valueOf(releaseID));
634     }
635   }
636   
637   private static final void validateAdministrativePage(final WebResponse page,
638                                                        final int projectID)
639     throws InvalidProjectIDException, 
640            PermissionDeniedException,
641            SourceForgeException {
642     if (page == null) {
643       throw new SourceForgeException(new NullPointerException("page == null"),
644                                      "The page argument cannot be null");
645     }
646     String title = null;
647     try {
648       title = page.getTitle();
649     } catch (final SAXException kaboom) {
650       throw new SourceForgeException(kaboom);
651     }
652     if (title == null || title.equals("SourceForge.net: Exiting with Error")) {
653       String text = null;
654       try {
655         text = page.getText();
656       } catch (final IOException kaboom) {
657         throw new SourceForgeException(kaboom);
658       }
659       assert text != null;
660       if (text.indexOf("Access to this page is restricted (either to project members or to project administrators)") >= 0) {
661         throw new PermissionDeniedException("Cannot access administrative " +
662                                             "interface of project " +
663                                             projectID);
664       } else {
665         try {
666           throw new SourceForgeException("SourceForge exited with an error: " + page.getText());
667         } catch (final IOException kaboom) {
668           throw new SourceForgeException("SourceForge exited with an error: " + page);
669         }
670       }
671     }
672   }
673 
674   private static final void validateFile(final File file)
675     throws InvalidFileException {
676     try {
677       FileSpecification.validate(file);
678     } catch (final Exception kaboom) {
679       throw new InvalidFileException(kaboom, file);
680     }
681   }
682 
683   /*
684    * Retrieval methods with HttpUnit-centric return types
685    */
686 
687   /*
688    * Page retrieval methods
689    */
690 
691   private final WebResponse getEditReleasePage(final FileRelease release)
692     throws NotLoggedInException,
693            InvalidProjectIDException,
694            InvalidPackageIDException,
695            InvalidFileReleaseIDException,
696            PermissionDeniedException,
697            SourceForgeUIChangeException,
698            SourceForgeException {
699     return this.getEditReleasePage(this.getProjectID(release),
700                                    this.getPackageID(release),
701                                    this.getFileReleaseID(release));
702   }
703 
704   private final WebResponse getEditReleasePage(final int projectID,
705                                                final int packageID,
706                                                final int releaseID)
707     throws NotLoggedInException,
708            InvalidProjectIDException,
709            InvalidPackageIDException,
710            InvalidFileReleaseIDException,
711            PermissionDeniedException,
712            SourceForgeUIChangeException,
713            SourceForgeException {
714     validateProjectID(projectID);
715     validatePackageID(packageID);
716     validateFileReleaseID(releaseID);
717     if (!this.isLoggedIn()) {
718       throw new NotLoggedInException();
719     }
720     final WebConversation wc = this.getWebConversation();
721     assert wc != null;
722     final GetMethodWebRequest request = 
723       new GetMethodWebRequest("https://sourceforge.net/project/admin/editreleases.php?package_id=" +
724                               packageID +
725                               "&release_id=" +
726                               releaseID +
727                               "&group_id=" +
728                               projectID);
729     WebResponse editReleasePage = null;
730     try {
731       editReleasePage = wc.getResponse(request);
732     } catch (final SAXException kaboom) {
733       throw new SourceForgeException(kaboom);
734     } catch (final IOException kaboom) {
735       throw new SourceForgeException(kaboom);
736     }
737     assert editReleasePage != null;
738     validateAdministrativePage(editReleasePage, projectID);
739     return editReleasePage;
740   }
741 
742   private final WebResponse getPackagesPage(final int projectID)
743     throws NotLoggedInException,
744            InvalidProjectIDException,
745            PermissionDeniedException,
746            SourceForgeUIChangeException,
747            SourceForgeException {
748     validateProjectID(projectID);
749     if (!this.isLoggedIn()) {
750       throw new NotLoggedInException();
751     }
752     final WebConversation wc = this.getWebConversation();
753     assert wc != null;
754     final GetMethodWebRequest request = 
755       new GetMethodWebRequest("http://sourceforge.net/project/admin/editpackages.php?group_id=" +
756                               projectID);
757     WebResponse packagesPage = null;
758     try {
759       packagesPage = wc.getResponse(request);
760     } catch (final SAXException kaboom) {
761       throw new SourceForgeException(kaboom);
762     } catch (final IOException kaboom) {
763       throw new SourceForgeException(kaboom);
764     }
765     assert packagesPage != null;
766     validateAdministrativePage(packagesPage, projectID);
767     returnong> packagesPage;
768   }
769   
770   private final WebResponse getReleasesPage(final int projectID,
771                                             final int packageID)
772     throws NotLoggedInException,
773            InvalidProjectIDException,
774            InvalidPackageIDException,
775            PermissionDeniedException,
776            SourceForgeUIChangeException,
777            SourceForgeException {
778     validateProjectID(projectID);
779     validatePackageID(packageID);
780     if (!this.isLoggedIn()) {
781       throw new NotLoggedInException();
782     }
783     final WebConversation wc = this.getWebConversation();
784     assert wc != null;
785     final GetMethodWebRequest request = 
786       new GetMethodWebRequest("http://sourceforge.net/project/admin/editreleases.php?package_id=" +
787                               packageID + "&group_id=" + projectID);
788     WebResponse releasesPage = null;
789     try {
790       releasesPage = wc.getResponse(request);
791     } catch (final SAXException kaboom) {
792       throw new SourceForgeException(kaboom);
793     } catch (final IOException kaboom) {
794       throw new SourceForgeException(kaboom);
795     }
796     assert releasesPage != null;
797     validateAdministrativePage(releasesPage, projectID);
798     return releasesPage;
799   }
800 
801   /*
802    * Form retrieval methods
803    */
804 
805   private final WebForm getEditExistingReleaseForm(final int projectID,
806                                                    final int packageID,
807                                                    final int releaseID)
808     throws NotLoggedInException,
809            InvalidProjectIDException,
810            InvalidPackageIDException,
811            InvalidFileReleaseIDException,
812            PermissionDeniedException,
813            SourceForgeUIChangeException,
814            SourceForgeException {
815     return 
816       this.getEditExistingReleaseForm(this.getEditReleasePage(projectID, 
817                                                               packageID,
818                                                               releaseID));
819   }
820   
821   private final WebForm getEditExistingReleaseForm(final WebResponse editReleasePage)
822     throws NotLoggedInException,
823            SourceForgeUIChangeException,
824            SourceForgeException {
825     if (editReleasePage == null) {
826       return null;
827     }
828     if (!this.isLoggedIn()) {
829       throw new NotLoggedInException();
830     }
831     WebForm[] forms = null;
832     try {
833       forms = this.retainFormsWithAction(editReleasePage.getForms(),
834                                          "/project/admin/editreleases.php");
835     } catch (final SAXException kaboom) {
836       throw new SourceForgeException(kaboom);
837     }
838     if (forms == null || forms.length <= 0) {
839       return null;
840     }
841     WebForm form;
842     for (int i = 0; i < forms.length; i++) {
843       form = forms[i];
844       if (form != null && form.hasParameterNamed("step1")) {
845         return form;
846       }
847     }
848     return null;
849   }
850 
851   private final WebForm getAddFilesForm(final int projectID,
852                                         final int packageID,
853                                         final int releaseID)
854     throws NotLoggedInException,
855            InvalidProjectIDException,
856            SourceForgeUIChangeException,
857            SourceForgeException {
858     return this.getAddFilesForm(this.getEditReleasePage(projectID, 
859                                                         packageID, 
860                                                         releaseID));
861   }
862 
863   private final WebForm getAddFilesForm(final WebResponse editReleasePage)
864     throws NotLoggedInException,
865            InvalidProjectIDException,
866            SourceForgeUIChangeException,
867            SourceForgeException {
868     if (editReleasePage == null) {
869       return null;
870     }
871     if (!this.isLoggedIn()) {
872       throw new NotLoggedInException();
873     }
874     WebForm[] forms = null;
875     try {
876       forms = this.retainFormsWithAction(editReleasePage.getForms(),
877                                          "/project/admin/editreleases.php");
878     } catch (final SAXException kaboom) {
879       throw new SourceForgeException(kaboom);
880     }
881     if (forms == null || forms.length <= 0) {
882       return null;
883     }
884     WebForm form;
885     for (int i = 0; i < forms.length; i++) {
886       form = forms[i];
887       if (form != null && form.hasParameterNamed("step2")) {
888         return form;
889       }
890     }
891     return null;
892   }
893 
894   private final WebForm getNotifyOthersForm(final int projectID,
895                                             final int packageID,
896                                             final int releaseID)
897     throws NotLoggedInException,
898            InvalidProjectIDException,
899            SourceForgeUIChangeException,
900            SourceForgeException {
901     return this.getNotifyOthersForm(this.getEditReleasePage(projectID, 
902                                                             packageID,
903                                                             releaseID));
904   }
905   
906   private final WebForm getNotifyOthersForm(final WebResponse editReleasePage)
907     throws NotLoggedInException,
908            InvalidProjectIDException,
909            SourceForgeUIChangeException,
910            SourceForgeException {
911     if (editReleasePage == null) {
912       return null;
913     }
914     if (!this.isLoggedIn()) {
915       throw new NotLoggedInException();
916     }
917     WebForm[] forms = null;
918     try {
919       forms = this.retainFormsWithAction(editReleasePage.getForms(),
920                                          "/project/admin/editreleases.php");
921     } catch (final SAXException kaboom) {
922       throw new SourceForgeException(kaboom);
923     }
924     if (forms == null || forms.length <= 0) {
925       return null;
926     }
927     WebForm form;
928     for (int i = 0; i < forms.length; i++) {
929       form = forms[i];
930       if (form != null && form.hasParameterNamed("step3")) {
931         return form;
932       }
933     }
934     return null;
935   }
936 
937   private final WebForm getUpdatePackageForm(final int projectID,
938                                              final int packageID)
939     throws NotLoggedInException,
940            InvalidProjectIDException,
941            InvalidPackageIDException,
942            SourceForgeUIChangeException,
943            SourceForgeException {
944     final WebForm[] updatePackageForms = this.getUpdatePackageForms(projectID);
945     if (updatePackageForms != null) {
946       WebForm form;
947       for (int i = 0; i < updatePackageForms.length; i++) {
948         form = updatePackageForms[i];
949         if (form != null && 
950             Integer.toString(packageID).equals(form.getParameterValue("package_id"))) {
951           return form;
952         }
953       }
954     }
955     return null;
956   }
957 
958   private final WebForm[] getUpdatePackageForms(final int projectID)
959     throws NotLoggedInException,
960            InvalidProjectIDException,
961            SourceForgeUIChangeException,
962            SourceForgeException {
963     return this.getUpdatePackageForms(this.getPackagesPage(projectID));
964   }
965 
966   privateg> final WebForm[] getUpdatePackageForms(final WebResponse packagesPage)
967     throws NotLoggedInException,
968            InvalidProjectIDException,
969            SourceForgeUIChangeException,
970            SourceForgeException {
971     ifong> (packagesPage == null) {
972       return new WebForm[0];
973     }
974     if (!this.isLoggedIn()) {
975       throw new NotLoggedInException();
976     }
977     WebForm[] allForms = null;
978     try {
979       allForms = packagesPage.getForms();
980     } catch (final SAXException kaboom) {
981       throw new SourceForgeException(kaboom);
982     }
983     final WebForm[] forms = 
984       this.retainFormsWithParameterNamed(allForms, "package_id");
985     if (forms == null || forms.length <= 0) {
986       throw new SourceForgeUIChangeException();
987     }
988     return forms;
989   }  
990 
991   /*
992    * Other HttpUnit-related retrieval methods
993    */
994 
995   private final WebTable getReleaseTable(final int projectID,
996                                          final int packageID)
997     throws NotLoggedInException,
998            InvalidProjectIDException,
999            InvalidPackageIDException,
1000            PermissionDeniedException,
1001            SourceForgeUIChangeException,
1002            SourceForgeException {
1003     finalong> WebResponse releasesPage = this.getReleasesPage(projectID, packageID);
1004     assert releasesPage != null;
1005     String text = null;
1006     try {
1007       text = releasesPage.getText();
1008     } catch (final IOException kaboom) {
1009       throw new SourceForgeException(kaboom);
1010     }
1011     if (text == null || 
1012         text.indexOf("You Have No Releases Of This Package Defined") >= 0) {
1013       return null;
1014     }
1015     WebTable releaseTable = null;
1016     try {
1017       releaseTable = releasesPage.getTableStartingWith("Release Name");
1018     } catch (final SAXException kaboom) {
1019       throw new SourceForgeException(kaboom);
1020     }
1021     if (releaseTable == null) {
1022       throw new SourceForgeUIChangeException();
1023     }
1024     return releaseTable;
1025   }
1026 
1027   /*
1028    * ID retrieval methods
1029    */
1030 
1031   private final int getFileReleaseID(final FileRelease fileRelease)
1032     throws InvalidFileReleaseIDException,
1033            TooManyFileReleasesException,
1034            SourceForgeException {
1035     assertNotNull(fileRelease, "fileRelease");
1036     final String idString = fileRelease.getID();
1037     final int id;
1038     if (idString == null) {
1039       id = this.getFileReleaseID(this.getProjectID(fileRelease),
1040                                  this.getPackageID(fileRelease),
1041                                  fileRelease.getName());
1042     } else {
1043       int tempID = -1;
1044       try {
1045         tempID = Integer.parseInt(idString);
1046       } catch (final NumberFormatException kaboom) {
1047         throw new InvalidFileReleaseIDException(kaboom, idString);
1048       } finally {
1049         id = tempID;
1050       }
1051     }
1052     this.validateFileReleaseID(id);
1053     if (idString == null) {
1054       fileRelease.setID(Integer.toString(id));
1055     }
1056     return id;
1057   }
1058 
1059   private final int getProjectID(final FileRelease release)
1060     throws InvalidProjectIDException, SourceForgeException {
1061     assertNotNull(release, "release");
1062     final Package pkg = release.getPackage();
1063     assertNotNull(pkg, "pkg");
1064     final Project project = pkg.getProject();
1065     assertNotNull(project, "project");
1066     return this.getProjectID(project);
1067   }
1068 
1069   private final int getProjectID(final Package pkg)
1070     throws InvalidProjectIDException, SourceForgeException {
1071     assertNotNull(pkg, "pkg");
1072     final Project project = pkg.getProject();
1073     assertNotNull(project, "project");
1074     return this.getProjectID(project);
1075   }
1076 
1077   private final int getProjectID(final Project project)
1078     throws InvalidProjectIDException, SourceForgeException {
1079     assertNotNull(project, "project");
1080     final String idString = project.getID();
1081     final int id;
1082     if (idString == null) {
1083       id = this.getProjectID(project.getShortName());
1084     } else {
1085       int tempID = -1;
1086       try {
1087         tempID = Integer.parseInt(idString);
1088       } catch (final NumberFormatException kaboom) {
1089         throw new InvalidProjectIDException(kaboom, idString);
1090       } finally {
1091         id = tempID;
1092       }
1093     }
1094     this.validateProjectID(id);
1095     if (idString == null) {
1096       project.setID(Integer.toString(id));
1097     }
1098     return id;
1099   }
1100 
1101   private final int getPackageID(final FileRelease release)
1102     throws InvalidPackageIDException, 
1103            InvalidPackageNameException,
1104            SourceForgeException {
1105     assertNotNull(release, "release");
1106     final Package pkg = release.getPackage();
1107     assertNotNull(pkg, "pkg");
1108     return this.getPackageID(pkg);
1109   }
1110 
1111   private final int getPackageID(final Package pkg)
1112     throws InvalidPackageIDException,
1113            InvalidPackageNameException,
1114            TooManyPackagesException,
1115            SourceForgeException {
1116     assertNotNull(pkg, "pkg");
1117     final String idString = pkg.getID();
1118     final int id;
1119     if (idString == null) {
1120       id = this.getPackageID(this.getProjectID(pkg), pkg.getName());
1121     } else {
1122       int tempID = -1;
1123       try {
1124         tempID = Integer.parseInt(idString);
1125       } catch (final NumberFormatException kaboom) {
1126         throw new InvalidPackageIDException(kaboom, idString);
1127       } finally {
1128         id = tempID;
1129       }
1130     }
1131     this.validatePackageID(id);
1132     if (idString == null) {
1133       pkg.setID(Integer.toString(id));
1134     }
1135     return id;
1136   }
1137 
1138   public int[] getPackageIDs(final int projectID)
1139     throws NotLoggedInException,
1140            InvalidProjectIDException,
1141            SourceForgeUIChangeException,
1142            SourceForgeException {
1143     final WebForm[] forms = this.getUpdatePackageForms(projectID);
1144     if (forms == null || forms.length <= 0) {
1145       throw new SourceForgeUIChangeException();
1146     }
1147     final int[] ids = new int[forms.length];
1148     WebForm form;
1149     String id;    
1150     Integer integerID;
1151     for (int i = 0; i < forms.length; i++) {
1152       form = forms[i];
1153       assert form != null;
1154       id = form.getParameterValue("package_id");
1155       if (id == null) {
1156         throw new SourceForgeException("Form found with no package ID!");
1157       }
1158       try {
1159         integerID = Integer.valueOf(id);
1160       } catch (final NumberFormatException kaboom) {
1161         throw new SourceForgeException(kaboom);
1162       }
1163       assert integerID != null;
1164       ids[i] = integerID.intValue();
1165     }
1166     return ids;
1167   }
1168   
1169   public int getPackageID(final int projectID,
1170                           final String packageName)
1171     throws NotLoggedInException,
1172            InvalidProjectIDException,
1173            InvalidPackageNameException,
1174            SourceForgeUIChangeException,
1175            TooManyPackagesException,
1176            SourceForgeException {
1177     returnong> this.getPackageID(projectID, packageName, false);
1178   }
1179 
1180   public int getPackageID(final int projectID,
1181                           final String packageName,
1182                           final boolean create)
1183     throws NotLoggedInException,
1184            InvalidProjectIDException,
1185            InvalidPackageNameException,
1186            SourceForgeUIChangeException,
1187            TooManyPackagesException,
1188            SourceForgeException {
1189     finalong> int[] ids = this.getPackageIDs(projectID, packageName, create);
1190     if (ids == null) {
1191       return -1;
1192     } else if (ids.length == 1) {
1193       return ids[0];
1194     } else if (ids.length <= 0) {
1195       return -1;
1196     } else {
1197       throwTooManyPackagesException(packageName)/package-summary.html">trong> new TooManyPackagesException(packageName);
1198     }
1199   }
1200 
1201   public int[] getPackageIDs(final int projectID,
1202                              final String packageName)
1203     throws NotLoggedInException,
1204            InvalidProjectIDException,
1205            InvalidPackageNameException,
1206            SourceForgeUIChangeException,
1207            SourceForgeException {
1208     returnong> this.getPackageIDs(projectID, packageName, false);
1209   }
1210 
1211   public int[] getPackageIDs(final int projectID, 
1212                              final String packageName,
1213                              final boolean create)
1214     throws NotLoggedInException,
1215            InvalidProjectIDException,
1216            InvalidPackageNameException,
1217            SourceForgeUIChangeException,
1218            SourceForgeException {
1219     final WebForm[] forms = this.getUpdatePackageForms(projectID);
1220     if (forms == null || forms.length <= 0) {
1221       throw new SourceForgeUIChangeException();
1222     }
1223     returnong> this.getPackageIDs(projectID, forms, packageName, create);
1224   }
1225 
1226   private final int[] getPackageIDs(final int projectID,
1227                                     final WebForm[] forms, 
1228                                     final String packageName,
1229                                     final boolean create)
1230     throws NotLoggedInException,
1231            InvalidProjectIDException,
1232            InvalidPackageNameException,
1233            SourceForgeUIChangeException,
1234            SourceForgeException {
1235     this.validatePackageName(packageName);
1236     if (forms == null || forms.length <= 0) {
1237       return new int[0];
1238     }
1239     WebForm form;
1240     String name;
1241     String value;
1242     Integer valueInteger;
1243     final SortedSet values = new TreeSet(REVERSE_COMPARATOR);
1244     for (int i = 0; i < forms.length; i++) {
1245       form = forms[i];
1246       if (form != null) {
1247         name = form.getParameterValue("package_name");
1248         if (name != null && name.equals(packageName)) {
1249           value = form.getParameterValue("package_id");
1250           if (value != null) {
1251             try {
1252               valueInteger = Integer.valueOf(value);
1253             } catch (final NumberFormatException ignore) {
1254               continue;
1255             }
1256             if (valueInteger != null) {
1257               values.add(valueInteger);
1258             }
1259           }
1260         }
1261       }
1262     }
1263     if (values.isEmpty()) {
1264       if (create) {
1265         final</strong> int id = this.createPackage(projectID, packageName, false);
1266         if (id >= 0) {
1267           return new int[] { id };
1268         }
1269       }
1270       return new int[0];
1271     }
1272     final int[] returnMe = new int[values.size()];
1273     final Iterator iterator = values.iterator();
1274     assert iterator != null;
1275     int i = 0;
1276     while (iterator.hasNext()) {
1277       valueInteger = (Integer)iterator.next();
1278       assert valueInteger != null;
1279       returnMe[i++] = valueInteger.intValue();
1280     }
1281     return returnMe;
1282   }
1283 
1284   private final int[] getPackageIDs(final int projectID,
1285                                     final WebResponse packagesPage,
1286                                     final String packageName,
1287                                     final boolean create)
1288     throws NotLoggedInException,
1289            InvalidProjectIDException,
1290            InvalidPackageNameException,
1291            SourceForgeUIChangeException,
1292            SourceForgeException {
1293     finalong> WebForm[] forms = this.getUpdatePackageForms(packagesPage);
1294     if (forms == null || forms.length <= 0) {
1295       return new int[0];
1296     }
1297     returnong> this.getPackageIDs(projectID, forms, packageName, create);
1298   }
1299 
1300   public int[] getFileReleaseIDs(final int projectID,
1301                                  final int packageID)
1302     throws NotLoggedInException,
1303            InvalidProjectIDException,
1304            InvalidPackageIDException,
1305            SourceForgeUIChangeException,
1306            SourceForgeException {
1307     LOGGER.info("Retrieving file release IDs for project " +
1308                 projectID +
1309                 " and package " + packageID);
1310     finalong> WebTable releaseTable = this.getReleaseTable(projectID, packageID);
1311     if (releaseTable == null) {
1312       return new int[0];
1313     }
1314     final int rowCount = releaseTable.getRowCount();
1315     final SortedSet ids = new TreeSet(REVERSE_COMPARATOR);
1316     TableCell cell;
1317     String cellText;
1318     WebLink link;
1319     String linkURL;
1320     Matcher matcher;
1321     String releaseIDString;
1322     for (int i = 1; i < rowCount; i++) {
1323       cell = releaseTable.getTableCell(i, 0);
1324       if (cell != null) {
1325         link = cell.getLinkWith("[Edit This Release]");
1326         if (link != null) {
1327           linkURL = link.getURLString();
1328           if (linkURL != null) {
1329             releaseIDString = null;
1330             synchronized (PATTERN_LOCK) {
1331               matcher = RELEASE_ID_PATTERN.matcher(linkURL);
1332               if (matcher != null && matcher.find()) {
1333                 releaseIDString = matcher.group(1);
1334               }
1335             }
1336             if (releaseIDString != null) {
1337               try {
1338                 ids.add(Integer.valueOf(releaseIDString));
1339               } catch (final NumberFormatException ignore) {
1340                 continue;
1341               }
1342             }
1343           }
1344         }
1345       }
1346     }
1347     if (ids.isEmpty()) {
1348       return new int[0];
1349     }
1350     final int[] returnMe = new int[ids.size()];
1351     final Iterator iterator = ids.iterator();
1352     assert iterator != null;
1353     for (int i = 0; iterator.hasNext(); i++) {
1354       returnMe[i] = ((Integer)iterator.next()).intValue();
1355     }
1356     return returnMe;
1357   }
1358 
1359   public int getFileReleaseID(final int projectID,
1360                               final int packageID,
1361                               final String fileReleaseName)
1362     throws NotLoggedInException,
1363            InvalidProjectIDException,
1364            InvalidPackageIDException,
1365            SourceForgeUIChangeException,
1366            TooManyFileReleasesException,
1367            SourceForgeException {
1368     returnong> this.getFileReleaseID(projectID, packageID, fileReleaseName, false);
1369   }
1370 
1371   public int getFileReleaseID(final int projectID,
1372                               final int packageID,
1373                               final String fileReleaseName,
1374                               final boolean create)
1375     throws NotLoggedInException,
1376            InvalidProjectIDException,
1377            InvalidPackageIDException,
1378            SourceForgeUIChangeException,
1379            TooManyFileReleasesException,
1380            SourceForgeException {
1381     final int[] ids = 
1382       this.getFileReleaseIDs(projectID, packageID, fileReleaseName, create);
1383     if (ids == null || ids.length <= 0) {
1384       return -1;
1385     } else if (ids.length == 1) {
1386       return ids[0];
1387     } else {
1388       throw new TooManyFileReleasesException(fileReleaseName);
1389     }
1390   }
1391 
1392   public int[] getFileReleaseIDs(final int projectID,
1393                                  final int packageID,
1394                                  final String fileReleaseName)
1395     throws NotLoggedInException,
1396            InvalidProjectIDException,
1397            InvalidPackageIDException,
1398            SourceForgeUIChangeException,
1399            SourceForgeException {
1400     returnong> this.getFileReleaseIDs(projectID, packageID, fileReleaseName, false);
1401   }
1402 
1403   public int[] getFileReleaseIDs(final int projectID,
1404                                  final int packageID,
1405                                  final String fileReleaseName,
1406                                  final boolean create)
1407     throws NotLoggedInException,
1408            InvalidProjectIDException,
1409            InvalidPackageIDException,
1410            SourceForgeUIChangeException,
1411            SourceForgeException {
1412     finalong> WebTable releaseTable = this.getReleaseTable(projectID, packageID);
1413     if (releaseTable == null) {
1414       return new int[0];
1415     }
1416     TableCell cell;
1417     String cellText;
1418     String releaseName;
1419     int leftBracketIndex;
1420     WebLink link;
1421     String linkURL;
1422     Matcher matcher;
1423     String releaseIDString;
1424     Integer idInteger;
1425     final SortedSet values = new TreeSet(REVERSE_COMPARATOR);
1426     final int rowCount = releaseTable.getRowCount();    
1427     for (int i = 1; i < rowCount; i++) {
1428       cell = releaseTable.getTableCell(i, 0);
1429       if (cell == null) {
1430         continue;
1431       }
1432       cellText = cell.asText();
1433       if (cellText == null) {
1434         continue;
1435       }
1436       leftBracketIndex = cellText.indexOf("[Edit This Release]");
1437       if (leftBracketIndex < 0) {
1438         continue;
1439       }
1440       releaseName = cellText.substring(0, leftBracketIndex);
1441       if (releaseName == null) {
1442         continue;
1443       }
1444       releaseName = releaseName.trim();
1445       assert releaseName != null;
1446       if (releaseName.equals(fileReleaseName)) {
1447         link = cell.getLinkWith("[Edit This Release]");
1448         if (link != null) {
1449           linkURL = link.getURLString();
1450           if (linkURL != null) {
1451             releaseIDString = null;
1452             synchronized (PATTERN_LOCK) {
1453               matcher = RELEASE_ID_PATTERN.matcher(linkURL);
1454               if (matcher != null && matcher.find()) {
1455                 releaseIDString = matcher.group(1);
1456               }
1457             }
1458             if (releaseIDString != null) {
1459               try {
1460                 idInteger = Integer.valueOf(releaseIDString);
1461               } catch (final NumberFormatException ignore) {
1462                 idInteger = null;
1463               }
1464               if (idInteger != null) {
1465                 values.add(idInteger);
1466               }
1467             }
1468           }
1469         }
1470       }
1471     }
1472     if (values.isEmpty()) {
1473       if (create) {
1474         final int id = 
1475           this.createFileRelease(projectID, packageID, fileReleaseName, false);
1476         if (id >= 0) {
1477           return new int[] { id };
1478         }
1479       }
1480       return new int[0];
1481     }
1482     final Iterator iterator = values.iterator();
1483     assert iterator != null;
1484     final int[] returnMe = new int[values.size()];
1485     int i = 0;
1486     while (iterator.hasNext()) {
1487       idInteger = (Integer)iterator.next();
1488       assert idInteger != null;
1489       returnMe[i++] = idInteger.intValue();
1490     }
1491     return returnMe;
1492   }
1493 
1494   /*
1495    * ID creation methods
1496    */
1497 
1498   public int createFileRelease(final int projectID,
1499                                final int packageID,
1500                                final String releaseName,
1501                                final boolean checkIfExists)
1502     throws NotLoggedInException,
1503            InvalidProjectIDException,
1504            PermissionDeniedException,
1505            SourceForgeUIChangeException,
1506            SourceForgeException {
1507     this.validateProjectID(projectID);
1508     this.validatePackageID(packageID);
1509     this.validateFileReleaseName(releaseName);
1510     if (!this.isLoggedIn()) {
1511       throw new NotLoggedInException();
1512     }
1513     if (checkIfExists) {
1514       final int[] ids = 
1515         this.getFileReleaseIDs(projectID, packageID, releaseName, false);
1516       if (ids != null && ids.length > 0) {
1517         return -1;
1518       }
1519     }
1520     final WebConversation wc = this.getWebConversation();
1521     assert wc != null;
1522     GetMethodWebRequest request = null;
1523     try {
1524       request =
1525         new GetMethodWebRequest("http://sourceforge.net/project/admin/newrelease.php?package_id=" +
1526                                 packageID + 
1527                                 "&group_id=" +
1528                                 projectID +
1529                                 "&release_name=" +
1530                                 URLEncoder.encode(releaseName, "UTF-8") +
1531                                 "&submit=Create+This+Release");
1532     } catch (final UnsupportedEncodingException wontHappen) {
1533       throw new SourceForgeException(wontHappen);
1534     }
1535     WebResponse response = null;
1536     try {
1537       response = wc.getResponse(request);
1538     } catch (final IOException kaboom) {
1539       throw new SourceForgeException(kaboom);
1540     } catch (final SAXException kaboom) {
1541       throw new SourceForgeException(kaboom);
1542     }
1543     assert response != null;
1544     validateAdministrativePage(response, projectID);
1545     final URL url = response.getURL();
1546     assert url != null;
1547     final String queryString = url.getQuery();
1548     assert queryString != null;
1549     final String releaseIDString;
1550     synchronized (PATTERN_LOCK) {
1551       final Matcher matcher = RELEASE_ID_PATTERN.matcher(queryString);
1552       if (matcher != null && matcher.find()) {
1553         releaseIDString = matcher.group(1);
1554       } else {
1555         releaseIDString = null;
1556       }
1557     }
1558     if (releaseIDString != null) {
1559       try {
1560         return Integer.parseInt(releaseIDString);
1561       } catch (final NumberFormatException kaboom) {
1562         throw new SourceForgeUIChangeException();
1563       }
1564     }
1565     return -1;
1566   }
1567 
1568   public int createPackage(final int projectID,
1569                            final String packageName,
1570                            final boolean checkIfExists)
1571     throws NotLoggedInException,
1572            InvalidProjectIDException,
1573            InvalidPackageNameException,
1574            PermissionDeniedException,
1575            SourceForgeUIChangeException,
1576            SourceForgeException {
1577     this.validateProjectID(projectID);
1578     this.validatePackageName(packageName);
1579     if (!this.isLoggedIn()) {
1580       throw new NotLoggedInException();
1581     }
1582     if (checkIfExists) {
1583       finaltrong> int[] ids = this.getPackageIDs(projectID, packageName, false);
1584       if (ids != null && ids.length > 0) {
1585         return -1;
1586       }
1587     }
1588     final WebConversation wc = this.getWebConversation();
1589     assert wc != null;
1590     GetMethodWebRequest request = null;
1591     try {
1592       request =
1593         new GetMethodWebRequest("http://sourceforge.net/project/admin/editpackages.php?group_id=" +
1594                                 projectID + 
1595                                 "&func=add_package&package_name=" +
1596                                 URLEncoder.encode(packageName, "UTF-8") +
1597                                 "&submit=Create+This+Package");
1598     } catch (final UnsupportedEncodingException wontHappen) {
1599       throw new SourceForgeException(wontHappen);
1600     }
1601     WebResponse response = null;
1602     try {
1603       response = wc.getResponse(request);
1604     } catch (final IOException kaboom) {
1605       throw new SourceForgeException(kaboom);
1606     } catch (final SAXException kaboom) {
1607       throw new SourceForgeException(kaboom);
1608     }
1609     assert response != null;
1610     validateAdministrativePage(response, projectID);
1611     String text = null;
1612     try {
1613       text = response.getText();
1614     } catch (final IOException kaboom) {
1615       throw new SourceForgeException(kaboom);
1616     }
1617     assert text != null;
1618     if (text.indexOf("Added Package") < 0) {
1619       throw new SourceForgeUIChangeException();
1620     }
1621     final int[] ids = 
1622       this.getPackageIDs(projectID, response, packageName, false);
1623     if (ids == null || ids.length <= 0) {
1624       return -1;
1625     }
1626     return ids[0];
1627   }
1628 
1629   /*
1630    * Other stuff
1631    */
1632   public String getPackageName(final int projectID,
1633                                final int packageID)
1634     throws NotLoggedInException,
1635            InvalidProjectIDException,
1636            InvalidPackageIDException,
1637            SourceForgeException {
1638     final WebForm updatePackageForm = 
1639       this.getUpdatePackageForm(projectID, packageID);
1640     finalong> String packageName;
1641     if (updatePackageForm != null) {
1642       packageName = updatePackageForm.getParameterValue("package_name");
1643     } else {
1644       packageName = null;
1645     }
1646     ifong> (packageName == null) {
1647       throwInvalidPackageIDException(packageID)/package-summary.html">trong> new InvalidPackageIDException(packageID);
1648     }
1649     returnong> packageName;
1650   }
1651 
1652   public void setPackageVisible(final Package pkg)
1653     throws NotLoggedInException,
1654            TooManyPackagesException,
1655            SourceForgeException {
1656     assertNotNull(pkg, "pkg");
1657     this.setPackageVisible(this.getProjectID(pkg),
1658                            this.getPackageID(pkg),
1659                            !pkg.isHidden());
1660   }
1661 
1662   public void setPackageVisible(final int projectID,
1663                                 final int packageID,
1664                                 final boolean visible)
1665     throws NotLoggedInException,
1666            SourceForgeException {
1667     this.setPackageVisible(projectID, 
1668                            packageID, 
1669                            this>.getPackageName(projectID, packageID),
1670                            visible);
1671   }
1672 
1673   private final void setPackageVisible(final int projectID,
1674                                        final int packageID,
1675                                        final String packageName,
1676                                        final boolean visible)
1677     throws NotLoggedInException,
1678            PermissionDeniedException,
1679            InvalidProjectIDException,
1680            InvalidPackageIDException,
1681            InvalidPackageNameException,
1682            SourceForgeUIChangeException,
1683            SourceForgeException {
1684     validateProjectID(projectID);
1685     validatePackageID(packageID);
1686     validatePackageName(packageName);
1687     if (!this.isLoggedIn()) {
1688       throw new NotLoggedInException();
1689     }
1690     final WebConversation wc = this.getWebConversation();
1691     assert wc != null;
1692     final String status;
1693     if (visible) {
1694       status = "3";
1695     } else {
1696       status = "1";
1697     }
1698     GetMethodWebRequest request = null;
1699     try {
1700       // How insanely stupid is it that the editpackages.php form won't update a
1701       // package unless it is given BOTH the ID AND the name?  You'd think the
1702       // ID would be enough....
1703       request =
1704         new GetMethodWebRequest("http://sourceforge.net/project/admin/editpackages.php?group_id=" +
1705                                 projectID + 
1706                                 "&func=update_package&package_name=" +
1707                                 URLEncoder.encode(packageName, "UTF-8") +
1708                                 "&status_id=" +
1709                                 status +
1710                                 "&submit=Update");
1711     } catch (final UnsupportedEncodingException wontHappen) {
1712       throw new SourceForgeException(wontHappen);
1713     }
1714     WebResponse response = null;
1715     try {
1716       response = wc.getResponse(request);
1717     } catch (final IOException kaboom) {
1718       throw new SourceForgeException(kaboom);
1719     } catch (final SAXException kaboom) {
1720       throw new SourceForgeException(kaboom);
1721     }
1722     assert response != null;
1723     validateAdministrativePage(response, projectID);
1724     if (!visible) {
1725       String text = null;
1726       try {
1727         text = response.getText();
1728       } catch (final IOException kaboom) {
1729         throw new SourceForgeException(kaboom);
1730       }
1731       if (text == null || 
1732           text.indexOf("Sorry - you cannot hide a package that contains active " +
1733                        "releases") >= 0) {
1734         // Go hide all the file releases
1735         final</strong> int[] ids = this.getFileReleaseIDs(projectID, packageID);
1736         if (ids != null && ids.length > 0) {
1737           for (int i = 0; i < ids.length; i++) {
1738             this.setFileReleaseVisible(projectID, packageID, ids[i], false);
1739           }
1740         }
1741         // Call ourselves again.  We won't enter this loop a second time.
1742         this.setPackageVisible(projectID, packageID, packageName, false);
1743       }
1744     }
1745   }
1746 
1747   public void setFileReleaseVisible(final FileRelease release)
1748     throws NotLoggedInException,
1749            TooManyPackagesException,
1750            TooManyFileReleasesException,
1751            SourceForgeUIChangeException,
1752            SourceForgeException {
1753     assertNotNull(release, "release");
1754     this.setFileReleaseVisible(this.getProjectID(release),
1755                                this.getPackageID(release),
1756                                this.getFileReleaseID(release),
1757                                !release.isHidden());
1758   }
1759 
1760   /***
1761    * Ensures that the file release with the supplied identifier is visible or
1762    * hidden.
1763    *
1764    * @param      projectID
1765    *               the project identifier to which the file release in question
1766    *               belongs; must be a positive integer greater than
1767    *               <code>0</code> 
1768    * @param      packageID
1769    *               the package identifier to which the file release in question
1770    *               belongs; must be a positive integer greater than
1771    *               <code>0</code>
1772    * @param      releaseID
1773    *               the identifier of the file release in question; must be a 
1774    *               positive integer greater than <code>0</code>
1775    * @param      visible
1776    *               whether the file release is to be visible or not
1777    * @exception  SourceForgeException
1778    *               if an error occurs
1779    */
1780   public void setFileReleaseVisible(final int projectID,
1781                                     final int packageID,
1782                                     final int releaseID,
1783                                     final boolean visible)
1784     throws SourceForgeException {
1785     final WebForm editExistingReleaseForm = 
1786       this.getEditExistingReleaseForm(projectID, 
1787                                       packageID,
1788                                       releaseID);
1789     if (editExistingReleaseForm != null) {
1790       if (visible) {
1791         editExistingReleaseForm.setParameter("status_id", "1");
1792       } else {
1793         editExistingReleaseForm.setParameter("status_id", "3");
1794       }
1795       try {
1796         editExistingReleaseForm.submit();
1797       } catch (final SAXException kaboom) {
1798         throw new SourceForgeException(kaboom);
1799       } catch (final IOException kaboom) {
1800         throw new SourceForgeException(kaboom);
1801       }
1802     }
1803   }
1804 
1805   /***
1806    * A convenience method that uploads the supplied {@link File}s to the
1807    * <code>incoming</code> directory on <code>upload.sourceforge.net</code> by
1808    * calling the {@link #uploadFile(File)} method on each element of the array.
1809    *
1810    * @param      files
1811    *               the {@link File}s to upload; may be <code>null</code> or
1812    *               empty in which case no action will be taken
1813    */
1814   public void uploadFiles(final File[] files) {
1815     if (files == null || files.length <= 0) {
1816       return;
1817     }
1818     File file;
1819     for (int i = 0; i < files.length; i++) {
1820       file = files[i];
1821       if (file != null) {
1822         this.uploadFile(file);
1823       }
1824     }
1825   }
1826 
1827   /***
1828    * Uploads the supplied {@link File} via FTP to the <code>incoming</code>
1829    * directory on <code>upload.sourceforge.net</code>.  This implementation
1830    * calls the {@link #uploadFileSynchronously(File)} method, but could be
1831    * overridden to call the {@link #uploadFileAsynchronously(File)} method
1832    * instead.
1833    *
1834    * @param      file
1835    *               the {@link File} to be uploaded; must not be
1836    *               <code>null</code>
1837    */
1838   public void uploadFile(final File file) {
1839     this.uploadFileSynchronously(file);
1840   }
1841 
1842   /***
1843    * Uploads the supplied {@link File} via FTP to the <code>incoming</code>
1844    * directory on <code>upload.sourceforge.net</code> and returns immediately.
1845    * Any {@linkplain #addFileUploadListener(FileUploadListener) registered
1846    * <code>FileUploadListener</code>}s are notified of a {@linkplain
1847    * FileUploadListener#fileUploaded(FileUploadEvent) successful upload} or any
1848    * {@linkplain FileUploadListener#exceptionCaught(FileUploadEvent)
1849    * <code>Exception</code> that is encountered} before this method returns.
1850    *
1851    * <p>This method calls the {@link #uploadFileSynchronously(File)} method in
1852    * the body of a new {@link Thread}'s {@link Thread#run()} method.</p>
1853    *
1854    * @param      file
1855    *               the {@link File} to be uploaded; must not be
1856    *               <code>null</code> 
1857    */
1858   public final void uploadFileAsynchronously(final File file) {
1859     new Thread() {
1860       public final void run() {
1861         uploadFileSynchronously(file);
1862       }
1863     }.start();
1864   }
1865 
1866   /***
1867    * Uploads the supplied {@link File} via FTP to the <code>incoming</code>
1868    * directory on <code>upload.sourceforge.net</code>, blocking until the {@link
1869    * File} is uploaded successfully.  Any {@linkplain
1870    * #addFileUploadListener(FileUploadListener) registered
1871    * <code>FileUploadListener</code>}s are notified of a {@linkplain
1872    * FileUploadListener#fileUploaded(FileUploadEvent) successful upload} or any
1873    * {@linkplain FileUploadListener#exceptionCaught(FileUploadEvent)
1874    * <code>Exception</code> that is encountered} before this method returns.
1875    *
1876    * @param      file
1877    *               the {@link File} to be uploaded; must not be
1878    *               <code>null</code> 
1879    */
1880   public final void uploadFileSynchronously(final File file) {
1881     InputStream inputStream = null;
1882     OutputStream outputStream = null;
1883     Exception exception = null;
1884     try {
1885       validateFile(file);
1886       LOGGER.info("Uploading " + file);
1887       inputStream = new BufferedInputStream(new FileInputStream(file));
1888       final String shortName = file.getName();
1889       assert shortName != null;
1890       final URLConnection connection =
1891         new URL("ftp://upload.sourceforge.net/incoming/" + 
1892                 shortName).openConnection();
1893       assert connection != null;      
1894       connection.setDoOutput(true);
1895       connection.connect();
1896       outputStream = connection.getOutputStream();
1897       if (!(outputStream instanceof BufferedOutputStream)) {
1898         outputStream = new BufferedOutputStream(outputStream);
1899       }
1900       copyStream(inputStream, outputStream);
1901       this.fireFileUploadedEvent(file);
1902       LOGGER.info("Successfully uploaded " + file);
1903     } catch (final Exception kaboom) {
1904       exception = kaboom;
1905       this.fireExceptionCaughtEvent(file, kaboom);
1906     } finally {
1907       try {
1908         if (outputStream != null) {
1909           outputStream.close();
1910         }
1911       } catch (final IOException ignore) {
1912         // ignore
1913       }
1914       try {
1915         if (inputStream != null) {
1916           inputStream.close();
1917         }
1918       } catch (final IOException ignore) {
1919         // ignore
1920       }
1921       if (exception != null) {
1922         LOGGER.severe("Failed to upload " + file + ": " + exception);
1923       }
1924     }
1925   }
1926 
1927   private synchronized final void ensureNoUploadErrors() throws SourceForgeException {
1928     if (this.uploadException != null) {
1929       if (this.uploadException instanceof SourceForgeException) {
1930         throw (SourceForgeException)this.uploadException;
1931       }
1932       throw new SourceForgeException(this.uploadException);
1933     }
1934   }
1935 
1936   /***
1937    * Assembles a {@link SortedSet} of {@linkplain File#getName()
1938    * <code>File</code> "short name"s} from the supplied {@link FileRelease}.
1939    * This method never returns <code>null</code>.
1940    *
1941    * @param      release
1942    *               the {@link FileRelease} to work on; must not be
1943    *               <code>null</code>
1944    * @return     a {@link SortedSet} of {@linkplain File#getName()
1945    *               <code>File</code> "short name"s} from the supplied {@link
1946    *               FileRelease}
1947    */
1948   private static final SortedSet extractShortFileNames(final FileRelease release) {
1949     final SortedSet names = new TreeSet();
1950     if (release != null) {
1951       names.addAll(Arrays.asList(release.getShortFileNames()));
1952     }
1953     return Collections.unmodifiableSortedSet(names);
1954   }
1955 
1956 
1957   /***
1958    * Copies the supplied {@link InputStream} to the supplied {@link
1959    * OutputStream} in chunks of 1,024 bytes.
1960    *
1961    * @param      inputStream
1962    *               the {@link InputStream} to copy; must not be
1963    *               <code>null</code>
1964    * @param      outputStream
1965    *               the {@link OutputStream} to copy the supplied {@link
1966    *               InputStream} to; must not be <code>null</code>
1967    * @exception  IOException
1968    *               if an input/output error occurs
1969    */
1970   private static final void copyStream(final InputStream inputStream,
1971                                        final OutputStream outputStream)
1972     throws IOException {
1973     if (inputStream == null || outputStream == null) {
1974       return;
1975     }
1976     final byte[] buffer = new byte[1024];
1977     int numberOfBytesRead = 0;
1978     while ((numberOfBytesRead = inputStream.read(buffer)) != -1) {
1979       outputStream.write(buffer, 0, numberOfBytesRead);
1980     }
1981     outputStream.flush();
1982   }
1983 
1984   private void login(final FileRelease release)
1985     throws InvalidCredentialsException, SourceForgeException {
1986     if (this.isLoggedIn()) {
1987       return;
1988     }
1989     assertNotNull(release, "release");
1990     final Package pkg = release.getPackage();
1991     assertNotNull(pkg, "pkg");
1992     final Project project = pkg.getProject();
1993     assertNotNull(project, "project");
1994     final Administrator admin = project.getAdministrator();
1995     assertNotNull(admin, "admin");
1996     final String userName = admin.getName();
1997     assertNotNull(userName, "userName");
1998     this.login(userName, admin.getPassword());
1999   }
2000   
2001   /***
2002    * A {@link Comparator} that provides the inverse of the result of comparing
2003    * two {@link Comparable}s according to their natural ordering.  This is a
2004    * fancy way of saying this class deliberately behaves exactly backwards,
2005    * i.e. common-sense "smaller" values are sorted as though they were "larger".
2006    *
2007    * <p>This class exists solely so that a method like {@link
2008    * FileReleaseSystem#getFileReleaseIDs(int, int, String)} will return the
2009    * most-recently-created identifier as the first element in the
2010    * <code>int</code> array it returns.</p>
2011    *
2012    * @author     <a href="mailto:ljnelson94@alumni.amherst.edu">Laird Nelson</a>
2013    * @version    $Revision: 1.21 $ $Date: 2004/07/27 20:20:35 $
2014    * @since      July 18, 2003 
2015    */
2016   static final class ReverseComparator implements Comparator {
2017     
2018     /***
2019      * Creates a new {@link FileReleaseSystem.ReverseComparator}.
2020      */
2021     ReverseComparator() {
2022       super();
2023     }
2024 
2025     /***
2026      * Compares the first argument to the second.  Returns a negative integer,
2027      * zero, or a positive integer as the first argument is <i>greater</i> than,
2028      * equal to, or less than the second.
2029      *
2030      * @param      one
2031      *               the first {@link Object} to compare; must be an instance 
2032      *               of {@link Comparable}
2033      * @param      two
2034      *               the second {@link Object} to compare; must be an instance 
2035      *               of {@link Comparable}
2036      * @return     a negative integer, zero, or a positive integer as the first
2037      *               argument is <i>greater</i> than, equal to, or less than the
2038      *               second
2039      * @exception  NullPointerException
2040      *               if <code>one</code> is <code>null</code>
2041      * @exception  ClassCastException
2042      *               if <code>one</code> is not an instance of {@link Comparable}
2043      */
2044     public final int compare(final Object one, final Object two) 
2045       throws NullPointerException, ClassCastException {
2046       return -((Comparable)one).compareTo(two);
2047     }
2048 
2049     /***
2050      * Tests the supplied {@link Object} to see if it is equal to this {@link
2051      * FileReleaseSystem.ReverseComparator}.  All instances of {@link
2052      * FileReleaseSystem.ReverseComparator} are considered equal.
2053      *
2054      * @param     anObject
2055      *              the {@link Object} to test; may be <code>null</code>
2056      * @return    <code>true</code> if the supplied {@link Object} is an
2057      *              instance of {@link FileReleaseSystem.ReverseComparator}
2058      */
2059     public final boolean equals(final Object anObject) {
2060       return anObject == this || anObject instanceof ReverseComparator;
2061     }
2062     
2063   }
2064 
2065 }