1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 package sfutils.frs.web;
29
30 import java.io.Serializable;
31 import java.io.FileInputStream;
32 import java.io.OutputStream;
33 import java.io.File;
34 import java.io.InputStream;
35 import java.io.IOException;
36
37 import java.net.MalformedURLException;
38 import java.net.URL;
39 import java.net.URLConnection;
40
41 import java.text.DateFormat;
42 import java.text.SimpleDateFormat;
43
44 import java.util.Arrays;
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.Collections;
48 import java.util.Date;
49 import java.util.SortedSet;
50 import java.util.TreeSet;
51
52 import java.util.logging.Logger;
53
54 import com.meterware.httpunit.ClientProperties;
55 import com.meterware.httpunit.GetMethodWebRequest;
56 import com.meterware.httpunit.HttpUnitOptions;
57 import com.meterware.httpunit.TableCell;
58 import com.meterware.httpunit.UploadFileSpec;
59 import com.meterware.httpunit.WebConversation;
60 import com.meterware.httpunit.WebForm;
61 import com.meterware.httpunit.WebLink;
62 import com.meterware.httpunit.WebRequest;
63 import com.meterware.httpunit.WebResponse;
64 import com.meterware.httpunit.WebTable;
65
66 import org.xml.sax.SAXException;
67
68 import sfutils.Administrator;
69 import sfutils.Project;
70
71 import sfutils.frs.FileRelease;
72 import sfutils.frs.FileSpecification;
73 import sfutils.frs.HideableNamedObject;
74 import sfutils.frs.Package;
75 import sfutils.frs.Publisher;
76 import sfutils.frs.PublishingException;
77
78 /***
79 * A {@link Publisher} that uses the <a
80 * href="http://httpunit.sourceforge.net/">HttpUnit</a> framework to interface
81 * with the <a href="http://sourceforge.net/">SourceForge</a> File Release
82 * System.
83 *
84 * @author <a href="mailto:ljnelson94@alumni.amherst.edu">Laird Nelson</a>
85 * @version $Revision: 1.15 $ $Date: 2004/07/27 20:20:34 $
86 * @since June 19, 2003
87 * @see Publisher
88 * @see FileRelease
89 * @see <a
90 * href="http://sourceforge.net/docman/display_doc.php?docid=6445&group_id=1">Guide
91 * to the SourceForge File Release System (FRS)</a>
92 * @todo Possibly reorganize to be less flow-driven and more stateless.
93 * Ideas include methods that extract identifiers for projects,
94 * packages and file releases, and methods that perform operations
95 * on projects, packages and file releases given their
96 * identifiers.
97 * @todo Introduce case insensitivity in appropriate places.
98 */
99 public class HttpUnitPublisher implements Publisher, Serializable {
100
101 /***
102 * The {@link String} that represents the URL to use to log in to <a
103 * href="http://sourceforge.net/">SourceForge</a>.
104 */
105 private static final String LOGIN_URL =
106 "http://sourceforge.net/account/login.php";
107
108 /***
109 * A {@link String} representing a package or file release's visibility
110 * status.
111 */
112 private static final String VISIBLE = "1";
113
114 /***
115 * A {@link String} that indicates that a value of a form has been turned on.
116 */
117 private static final String ON = "1";
118
119 /***
120 * A {@link String} that corresponds to a file release's or package's hidden
121 * status.
122 */
123 private static final String HIDDEN = "3";
124
125 /***
126 * The name of a form parameter in the <a
127 * href="http://sourceforge.net/">SourceForge</a> file release system whose
128 * value indicates what action to take next.
129 */
130 private static final String PACKAGE_ACTION_PARAMETER = "func";
131
132 /***
133 * One possible value for the form parameter whose name is equal to the value
134 * of the {@link #PACKAGE_ACTION_PARAMETER} field.
135 */
136 private static final String UPDATE_PACKAGE = "update_package";
137
138 /***
139 * One possible value for the form parameter whose name is equal to the value
140 * of the {@link #PACKAGE_ACTION_PARAMETER} field.
141 */
142 private static final String ADD_PACKAGE = "add_package";
143
144 /***
145 * A form parameter name whose corresponding value will be the name of a
146 * package.
147 */
148 private static final String PACKAGE_NAME = "package_name";
149
150 /***
151 * A form parameter name whose corresponding value will be the identifier of a
152 * package.
153 */
154 private static final String PACKAGE_ID = "package_id";
155
156 /***
157 * A form parameter name whose corresponding value will be the identifier of a
158 * project.
159 */
160 private static final String GROUP_ID = "group_id";
161
162 /***
163 * A form parameter name whose corresponding value will be either {@link
164 * #HIDDEN} or {@link #VISIBLE}.
165 */
166 private static final String STATUS = "status_id";
167
168 /***
169 * A form parameter name whose corresponding value will be the name of a file
170 * release.
171 */
172 private static final String RELEASE_NAME = "release_name";
173
174 /***
175 * A form parameter name whose corresponding value will be the release date of
176 * a file release.
177 */
178 private static final String RELEASE_DATE = "release_date";
179
180 /***
181 * A form parameter name whose corresponding value will be a {@link File} that
182 * will be uploaded to the <a href="http://sourceforge.net/">SourceForge</a>
183 * website to describe the changes in a given file release.
184 */
185 private static final String UPLOAD_CHANGE_LOG = "uploaded_changes";
186
187 /***
188 * A form parameter name whose corresponding value will be a {@link File} that
189 * will be uploaded to the <a href="http://sourceforge.net/">SourceForge</a>
190 * website to describe the changes in a given file release from one version to
191 * the next.
192 */
193 private static final String UPLOAD_RELEASE_NOTES = "uploaded_notes";
194
195 /***
196 * A form parameter name whose value will be the contents of a changelog.
197 */
198 private static final String CHANGE_LOG = "release_changes";
199
200 /***
201 * A form parameter name whose value will be the contents of a release notes
202 * file.
203 */
204 private static final String RELEASE_NOTES = "release_notes";
205
206 /***
207 * A form parameter name whose value will indicate whether preformatted text
208 * is to be preserved. Its value may be equal to the value of the {@link #ON}
209 * field.
210 */
211 private static final String PRESERVE_FORMATTED_TEXT = "preformatted";
212
213 /***
214 * An <code>int</code> that corresponds to the step on the "edit release" page
215 * that will actually edit the file release in question.
216 */
217 private static final int EDIT_RELEASE_STEP = 1;
218
219 /***
220 * An <code>int</code> that corresponds to the step on the "edit release" page
221 * that will select files to be included as part of the file release in
222 * question.
223 */
224 private static final int ADD_FILES_STEP = 2;
225
226 /***
227 * An <code>int</code> that corresponds to the step on the "edit release" page
228 * that will cause other <a href="http://sourceforge.net/">SourceForge</a>
229 * users monitoring the file release in question to be notified by email of
230 * its release.
231 */
232 private static final int NOTIFY_OTHERS_STEP = 4;
233
234 /***
235 * A relative URL {@link String} used as the value for a certain form's
236 * <code>ACTION</code> attribute when the form is designed to return a page
237 * that allows the user to edit the file releases that belong to a package.
238 */
239 private static final String EDIT_RELEASES_ACTION =
240 "/project/admin/editreleases.php";
241
242 /***
243 * A relative URL {@link String} used as the value for a certain form's
244 * <code>ACTION</code> attribute when the form is designed to return a page
245 * that allows the user to edit the packages that belong to a project.
246 */
247 private static final String EDIT_PACKAGES_ACTION =
248 "/project/admin/editpackages.php";
249
250 /***
251 * The {@link Logger} used by all instances of this class. This field is
252 * never <code>null</code>.
253 */
254 private static final Logger LOGGER;
255
256 /***
257 * The {@link DateFormat} used to format release dates. This field is never
258 * <code>null</code> and <i>must</i> be synchronized on before use.
259 */
260 private static final DateFormat DATE_FORMATTER;
261
262 /***
263 * Static initializer; initializes the {@link Logger} used by this class as
264 * well as other private fields.
265 */
266 static {
267 LOGGER = Logger.getLogger(HttpUnitPublisher.class.getName());
268 DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd");
269 HttpUnitOptions.setExceptionsThrownOnScriptError(false);
270 HttpUnitOptions.setScriptingEnabled(false);
271 }
272
273 /***
274 * Creates a new {@link HttpUnitPublisher}.
275 */
276 public HttpUnitPublisher() {
277 super();
278 }
279
280 /***
281 * Publishes the supplied {@link FileRelease} to its associated {@link
282 * Project} area on <a href="http://sourceforge.net/">SourceForge</a>.
283 *
284 * @param release
285 * the {@link FileRelease} to publish; must not be
286 * <code>null</code>
287 * @exception PublishingException
288 * if the supplied {@link FileRelease} could not be published
289 */
290 public void publish(final FileRelease release)
291 throws PublishingException {
292
293
294 assertNotNull(release, "release");
295 final FileSpecification[] files = release.getFileSpecifications();
296 assertArrayFull(files, "files");
297 final Package pkg = release.getPackage();
298 assertNotNull(pkg, "pkg");
299 final Project project = pkg.getProject();
300 assertNotNull(project, "project");
301 final String projectShortName = project.getShortName();
302 assertNotNull(projectShortName, "projectShortName");
303 final Administrator admin = project.getAdministrator();
304 assertNotNull(admin, "admin");
305 final String userName = admin.getName();
306 assertNotNull(userName, "userName");
307 final String password = admin.getPassword();
308
309
310 final WebConversation conversation = new WebConversation();
311 final ClientProperties clientProperties = conversation.getClientProperties();
312 if (clientProperties != null) {
313 clientProperties.setAcceptGzip(false);
314 }
315
316 try {
317
318
319 final WebResponse loginResponse = this.login(conversation, project);
320 assertNotNull(loginResponse, "loginResponse");
321 LOGGER.info("Retrieved " + loginResponse.getTitle() +
322 " (result of logging in)");
323 LOGGER.info("Logged in as " + userName);
324
325
326 final WebResponse summaryPage =
327 this.getSummaryPage(conversation, projectShortName);
328 assertNotNull(summaryPage, "summaryPage");
329 LOGGER.info("Retrieved " + summaryPage.getTitle() + " (summary page)");
330
331
332 final WebResponse adminPage = this.getAdminPage(summaryPage);
333 assertNotNull(adminPage, "adminPage");
334 LOGGER.info("Retrieved " + adminPage.getTitle() + " (admin page)");
335
336
337 final WebResponse packagesPage = this/getPackagesPage(adminPage)/package-summary.html">trong> WebResponse packagesPage = this.getPackagesPage(adminPage);
338 assertNotNull(packagesPage, "packagesPage");
339 LOGGER.info("Retrieved " + packagesPage.getTitle() + " (packages page)");
340
341
342
343 this.uploadFiles(release);
344
345
346 WebResponse editReleasePage =
347 this.getEditReleasePage(conversation,
348 packagesPage,
349 release);
350 assertNotNull(editReleasePage, "editReleasePage");
351 LOGGER.info("Retrieved " + editReleasePage.getTitle() +
352 " (packages page)");
353
354
355
356 this.processFileRelease(editReleasePage, release);
357 LOGGER.info("Finished processing release.");
358
359 } catch (final SAXException wrapMe) {
360 throw new PublishingException(wrapMe);
361 }
362
363 }
364
365 /***
366 * Ensures that all the properties and attributes specified in the supplied
367 * {@link FileRelease} are saved persistently to <a
368 * href="http://sourceforge.net/">SourceForge</a>. This method is called by
369 * the {@link #processFileRelease(WebResponse, FileRelease)} method.
370 *
371 * <p>This method never returns <code>null</code>.</p>
372 *
373 * @param editReleasePage
374 * the {@link WebResponse} that corresponds to the edit release
375 * page in <a href="http://sourceforge.net/">SourceForge</a>'s
376 * file release system; must not be <code>null</code>
377 * @param release
378 * the {@link FileRelease} to save; must not be
379 * <code>null</code>
380 * @return what amounts to the same page as the supplied {@link
381 * WebResponse} that hopefully now reflects the attributes and
382 * properties of the supplied {@link FileRelease}; never
383 * <code>null</code>
384 * @exception PublishingException
385 * if an error occurs
386 */
387 protected WebResponse saveFileReleaseAttributes(WebResponse editReleasePage,
388 final FileRelease release)
389 throws PublishingException {
390 assertNotNull(editReleasePage, "editReleasePage");
391 assertNotNull(release, "release");
392 final String formattedDate = this.formatReleaseDate(release);
393 assertNotNull(formattedDate, "formattedDate");
394 try {
395 final WebForm step1Form =
396 this.getStepForm(editReleasePage, EDIT_RELEASE_STEP);
397 assertNotNull(step1Form, "step1Form");
398 step1Form.setParameter(RELEASE_DATE, formattedDate);
399 if (release.isHidden()) {
400 step1Form.setParameter(STATUS, HIDDEN);
401 } else {
402 step1Form.setParameter(STATUS, VISIBLE);
403 }
404 if (release.getPreserveFormattedText()) {
405 step1Form.setParameter(PRESERVE_FORMATTED_TEXT, ON);
406 } else {
407 step1Form.removeParameter(PRESERVE_FORMATTED_TEXT);
408 }
409 final File changeLogFile = release.getChangeLogFile();
410 if (changeLogFile != null && changeLogFile.canRead()) {
411 step1Form.setParameter(UPLOAD_CHANGE_LOG,
412 new UploadFileSpec[] {
413 new UploadFileSpec(changeLogFile)
414 });
415 } else {
416 final String changeLogText = release.getChangeLog();
417 if (changeLogText != null) {
418 step1Form.setParameter(CHANGE_LOG, changeLogText);
419 }
420 }
421 final File releaseNotesFile = release.getReleaseNotesFile();
422 if (releaseNotesFile != null && releaseNotesFile.canRead()) {
423 step1Form.setParameter(UPLOAD_RELEASE_NOTES,
424 new UploadFileSpec[] {
425 new UploadFileSpec(releaseNotesFile)
426 });
427 } else {
428 final String releaseNotesText = release.getReleaseNotes();
429 if (releaseNotesText != null) {
430 step1Form.setParameter(RELEASE_NOTES, releaseNotesText);
431 }
432 }
433 editReleasePage = step1Form.submit();
434 assertNotNull(editReleasePage, "editReleasePage");
435 return editReleasePage;
436 } catch (final SAXException wrapMe) {
437 throw new PublishingException(wrapMe);
438 } catch (final IOException wrapMe) {
439 throw new PublishingException(wrapMe);
440 }
441 }
442
443 /***
444 * Formats the {@linkplain FileRelease#getReleaseDate() release date
445 * associated with the supplied <code>FileRelease</code>}. This method may
446 * return <code>null</code>.
447 *
448 * @param release
449 * the {@link FileRelease} whose {@linkplain
450 * FileRelease#getReleaseDate() release date} will be formatted;
451 * if <code>null</code>, then <code>null</code> will be returned
452 * @return the formatted {@link Date}, or <code>null</code>
453 */
454 protected String formatReleaseDate(final FileRelease release) {
455 if (release == null) {
456 return null;
457 }
458 final Date releaseDate = release.getReleaseDate();
459 if (releaseDate == null) {
460 return null;
461 }
462 synchronized (DATE_FORMATTER) {
463 return DATE_FORMATTER.format(releaseDate);
464 }
465 }
466
467 /***
468 * Works on the "edit release" page so that the supplied {@link FileRelease}
469 * is effectively saved to <a href="http://sourceforge.net/">SourceForge</a>.
470 * Returns the "edit release" page after all edits have been made.
471 *
472 * <p>This method is called by the {@link #publish(FileRelease)} method and
473 * will never return <code>null</code>.</p>
474 *
475 * @param editReleasePage
476 * a {@link WebResponse} that represents the "edit release"
477 * page; must not be <code>null</code>
478 * @param release
479 * the {@link FileRelease} to be released; must not be
480 * <code>null</code>
481 * @return a {@link WebResponse} representing the "edit release" page
482 * after all edits have been made; never <code>null</code>
483 * @exception PublishingException
484 * if an error occurs
485 */
486 protected WebResponse processFileRelease(WebResponse editReleasePage,
487 final FileRelease release)
488 throws PublishingException {
489 editReleasePage = this.saveFileReleaseAttributes(editReleasePage, release);
490 editReleasePage = this.addFiles(editReleasePage, release);
491 editReleasePage = this.editFiles(editReleasePage, release);
492 editReleasePage = this.notifyOthers(editReleasePage, release);
493 return editReleasePage;
494 }
495
496 /***
497 * Ensures that other <a href="http://sourceforge.net/">SourceForge</a> users
498 * who have expressed interest are notified when the supplied {@link
499 * FileRelease} is released. Returns the "edit release" page after all edits
500 * have been made. This method is called by the {@link
501 * #processFileRelease(WebResponse, FileRelease)} method and never returns
502 * <code>null</code>.
503 *
504 * @param editReleasePage
505 * a {@link WebResponse} that represents the "edit release"
506 * page; must not be <code>null</code>
507 * @param release
508 * the {@link FileRelease} to be released; must not be
509 * <code>null</code>
510 * @return a {@link WebResponse} representing the "edit release" page
511 * after all edits have been made; never <code>null</code>
512 * @exception PublishingException
513 * if an error occurs
514 */
515 protected WebResponse notifyOthers(WebResponse editReleasePage,
516 final FileRelease release)
517 throws PublishingException {
518 assertNotNull(editReleasePage, "editReleasePage");
519 assertNotNull(release, "release");
520 if (release.getNotifyOthers()) {
521 final WebForm form =
522 this.getStepForm(editReleasePage, NOTIFY_OTHERS_STEP);
523 if (form != null) {
524 try {
525 editReleasePage = form.submit();
526 } catch (final SAXException kaboom) {
527 throw new PublishingException(kaboom);
528 } catch (final IOException kaboom) {
529 throw new PublishingException(kaboom);
530 }
531 }
532 }
533 return editReleasePage;
534 }
535
536 /***
537 * Adds the {@link File}s that were uploaded earlier in the publishing process
538 * to the <a href="http://sourceforge.net/">SourceForge</a> representation of
539 * the supplied {@link FileRelease}. This method works on the "edit release"
540 * page and makes the necessary edits there before returning a {@link
541 * WebResponse} representing the "edit release" page with all the edits
542 * complete.
543 *
544 * <p>This method is called by the {@link #processFileRelease(WebResponse,
545 * FileRelease)} method and will never return <code>null</code>.</p>
546 *
547 * @param editReleasePage
548 * a {@link WebResponse} that represents the "edit release"
549 * page; must not be <code>null</code>
550 * @param release
551 * the {@link FileRelease} to be released; must not be
552 * <code>null</code>
553 * @return a {@link WebResponse} representing the "edit release" page
554 * after all edits have been made; never <code>null</code>
555 * @exception PublishingException
556 * if an error occurs
557 */
558 protected WebResponse addFiles(WebResponse editReleasePage,
559 final FileRelease release)
560 throws PublishingException {
561 assertNotNull(editReleasePage, "editReleasePage");
562 assertNotNull(release, "release");
563 final FileSpecification[] specs = release.getFileSpecifications();
564 assertArrayFull(specs, "specs");
565
566 try {
567 WebForm form = this.getStepForm(editReleasePage, ADD_FILES_STEP);
568 assertNotNull(form, "form");
569
570
571
572 editReleasePage = form.submit();
573 assertNotNull(editReleasePage, "editReleasePage");
574
575 form = this.getStepForm(editReleasePage, ADD_FILES_STEP);
576 assertNotNull(form, "form");
577
578
579
580 final SortedSet shortFileNames = this.extractShortFileNames(release);
581 assertNotNull(shortFileNames, "shortFileNames");
582 LOGGER.info("Setting file_list[] to " + shortFileNames);
583 form.setParameter("file_list[]",
584 (String[])shortFileNames.toArray(new String[shortFileNames.size()]));
585
586 editReleasePage = form.submit();
587 assertNotNull(editReleasePage, "editReleasePage");
588 return editReleasePage;
589
590 } catch (final IOException kaboom) {
591 throw new PublishingException(kaboom);
592 } catch (final SAXException kaboom) {
593 throw new PublishingException(kaboom);
594 }
595
596 }
597
598 /***
599 * Edits the attributes of the {@link File}s that were uploaded earlier in the
600 * publishing process. This method works on the "edit release"
601 * page and makes the necessary edits there before returning a {@link
602 * WebResponse} representing the "edit release" page with all the edits
603 * complete.
604 *
605 * <p>This method is called by the {@link #processFileRelease(WebResponse,
606 * FileRelease)} method and will never return <code>null</code>.</p>
607 *
608 * @param editReleasePage
609 * a {@link WebResponse} that represents the "edit release"
610 * page; must not be <code>null</code>
611 * @param release
612 * the {@link FileRelease} to be released; must not be
613 * <code>null</code>
614 * @return a {@link WebResponse} representing the "edit release" page
615 * after all edits have been made; never <code>null</code>
616 * @exception PublishingException
617 * if an error occurs
618 */
619 protected WebResponse editFiles(WebResponse editReleasePage,
620 final FileRelease release)
621 throws PublishingException {
622 assertNotNull(editReleasePage, "editReleasePage");
623 assertNotNull(release, "release");
624 try {
625 WebForm[] forms =
626 this.retainFormsWithAction(editReleasePage.getForms(),
627 EDIT_RELEASES_ACTION);
628 assertArrayFull(forms, "forms");
629 if (forms.length < 2) {
630 throw new PublishingException("Expected at least two forms");
631 } else if (forms.length == 2) {
632
633 return editReleasePage;
634 }
635
636
637
638
639
640 WebTable table = editReleasePage.getTableStartingWithPrefix("Filename");
641 assertNotNull(table, "table");
642
643 WebForm form;
644 String title;
645 FileSpecification spec;
646 int formIndex = 2;
647 int tableRowIndex = 1;
648 while (formIndex < forms.length) {
649 form = forms[formIndex];
650 assertNotNull(form, "form");
651 if (form.hasParameterNamed("im_sure")) {
652 tableRowIndex += 3;
653 } else if (form.hasParameterNamed("processor_id") &&
654 form.hasParameterNamed("type_id")) {
655 title = table.getCellAsText(tableRowIndex, 0);
656 assertNotNull(title, "title");
657 spec = release.getFileSpecification(title);
658 assertNotNull(spec, "spec");
659 form.setParameter("processor_id",
660 String.valueOf(spec.getProcessorType()));
661 form.setParameter("type_id", String.valueOf(spec.getFileType()));
662 editReleasePage = form.submit();
663 assertNotNull(editReleasePage, "editReleasePage");
664 forms =
665 this.retainFormsWithAction(editReleasePage.getForms(),
666 EDIT_RELEASES_ACTION);
667 assertArrayFull(forms, "forms");
668 table = editReleasePage.getTableStartingWithPrefix("Filename");
669 assertNotNull(table, "table");
670 } else {
671
672 }
673 formIndex++;
674 }
675
676 return editReleasePage;
677 } catch (final IOException kaboom) {
678 throw new PublishingException(kaboom);
679 } catch (final SAXException kaboom) {
680 throw new PublishingException(kaboom);
681 }
682 }
683
684 /***
685 * A convenience method that returns a {@link WebForm} representing the form
686 * on the "edit release" page with the supplied "step". For example, the
687 * {@link WebForm} corresponding to the form labeled "Step 1" will be returned
688 * if the supplied step is equal to <code>1</code>. This method is called by
689 * the {@link #addFiles(WebResponse, FileRelease)}, {@link
690 * #notifyOthers(WebResponse, FileRelease)} and {@link
691 * #saveFileReleaseAttributes(WebResponse, FileRelease)} methods and may
692 * return <code>null</code>.
693 *
694 * @param editReleasePage
695 * a {@link WebResponse} that represents the "edit release"
696 * page; must not be <code>null</code>
697 * @param step
698 * the "step number" of the form on the "edit release" page that
699 * chooses the {@link WebForm} to be returned; must be greater
700 * than or equal to <code>1</code>
701 * @return an appropriate {@link WebForm}, or <code>null</code>
702 * @exception PublishingException
703 * if an error occurs
704 */
705 protected WebForm getStepForm(final WebResponse editReleasePage,
706 final int step)
707 throws PublishingException {
708 assertNotNull(editReleasePage, "editReleasePage");
709 WebForm[] forms = null;
710 try {
711 forms = this.retainFormsWithAction(editReleasePage.getForms(),
712 EDIT_RELEASES_ACTION);
713 } catch (final SAXException kaboom) {
714 throw new PublishingException(kaboom);
715 }
716 assertArrayFull(forms, "forms");
717 WebForm form;
718 for (int i = 0; i < forms.length; i++) {
719 form = forms[i];
720 if (form != null && form.hasParameterNamed("step" + step)) {
721 return form;
722 }
723 }
724 return null;
725 }
726
727 /***
728 * Uploads all {@link File}s that are reachable from the supplied {@link
729 * FileRelease} to <code>ftp://upload.sourceforge.net/incoming/</code>. The
730 * default implementation of this method creates and starts a new {@link
731 * HttpUnitPublisher.FileUploader} for each {@link File}, so it is tacitly
732 * assumed that the total number of {@link File}s to be uploaded will be
733 * relatively small. This method is called by the {@link
734 * #publish(FileRelease)} method.
735 *
736 * @param release
737 * the {@link FileRelease} containing {@link FileSpecification}s
738 * containing the {@link File}s to be uploaded; must not be
739 * <code>null</code>
740 * @exception PublishingException
741 * if an error occurs
742 */
743 public void uploadFiles(final FileRelease release)
744 throws PublishingException {
745 assertNotNull(release, "release");
746 final File[] files = release.getFiles();
747 assertArrayFull(files, "files");
748 File file;
749 String shortName;
750 URL url;
751 URLConnection connection;
752 final Thread[] threads = new Thread[files.length];
753 Thread thread;
754 final Collection errors = Collections.synchronizedList(new ArrayList());
755 try {
756 for (int i = 0; i < files.length; i++) {
757 file = files[i];
758 if (file != null && file.canRead()) {
759 thread = new FileUploader(file, errors);
760 thread.start();
761 threads[i] = thread;
762 }
763 }
764 for (int i = 0; i < threads.length; i++) {
765 threads[i].join();
766 }
767 if (!errors.isEmpty()) {
768 final Exception[] exceptions =
769 (Exception[])errors.toArray(new Exception[errors.size()]);
770 throw new UploadException(exceptions);
771 }
772 } catch (final InterruptedException ignore) {
773 throw new UploadException(new InterruptedException[] { ignore });
774 }
775 }
776
777 /***
778 * Assembles a {@link SortedSet} of {@linkplain File#getName()
779 * <code>File</code> "short name"s} from the supplied {@link FileRelease}.
780 * This method is called by the {@link #addFiles(WebResponse, FileRelease)}
781 * method and never returns <code>null</code>.
782 *
783 * @param release
784 * the {@link FileRelease} to work on; must not be
785 * <code>null</code>
786 * @return a {@link SortedSet} of {@linkplain File#getName()
787 * <code>File</code> "short name"s} from the supplied {@link
788 * FileRelease}
789 * @exception PublishingException
790 * if an error occurs
791 */
792 protected SortedSet extractShortFileNames(final FileRelease release)
793 throws PublishingException {
794 assertNotNull(release, "release");
795 final SortedSet names = new TreeSet();
796 names.addAll(Arrays.asList(release.getShortFileNames()));
797 return names;
798 }
799
800 /***
801 * Returns a {@link WebResponse} that represents the "summary page" for the
802 * supplied <a href="http://sourceforge.net/">SourceForge</a> project. This
803 * method will never return <code>null</code>.
804 *
805 * @param conversation
806 * the {@link WebConversation} to which all interaction with <a
807 * href="http://sourceforge.net/">SourceForge</a> logically
808 * belongs; must not be <code>null</code>
809 * @param projectShortName
810 * the "short name" of the <a
811 * href="http://sourceforge.net/">SourceForge</a> project whose
812 * summary page should be returned; must not be
813 * <code>null</code>
814 * @return a {@link WebResponse} corresponding to the "summary page" for
815 * the supplied <a
816 * href="http://sourceforge.net/">SourceForge</a> project; never
817 * <code>null</code>
818 * @exception PublishingException
819 * if an error occurs
820 */
821 protected WebResponse getSummaryPage(final WebConversation conversation,
822 final String projectShortName)
823 throws PublishingException {
824 assertNotNull(conversation, "conversation");
825 assertNotNull(projectShortName, "projectShortName");
826
827 final WebRequest mainProjectPageRequest =
828 new GetMethodWebRequest("http://sourceforge.net/projects/" +
829 projectShortName +
830 "/");
831 try {
832 final WebResponse returnMe =
833 conversation.getResponse(mainProjectPageRequest);
834 assertNotNull(returnMe, "returnMe");
835 return returnMe;
836 } catch (final IOException kaboom) {
837 throw new PublishingException(kaboom);
838 } catch (final SAXException wrapMe) {
839 throw new PublishingException(wrapMe);
840 }
841 }
842
843 /***
844 * Returns a {@link WebResponse} that represents the "admin page" for the <a
845 * href="http://sourceforge.net/">SourceForge</a> project summarized in the
846 * supplied {@link WebResponse} that must have been supplied by the {@link
847 * #getSummaryPage(WebConversation, String)} method. This method will never
848 * return <code>null</code>.
849 *
850 * @param summaryPage
851 * the {@link WebResponse} returned by the {@link
852 * #getSummaryPage(WebConversation, String)} method; must not be
853 * <code>null</code>
854 * @return a {@link WebResponse} that represents the "admin page" for the
855 * <a href="http://sourceforge.net/">SourceForge</a> project
856 * summarized in the supplied {@link WebResponse}; never
857 * <code>null</code>
858 * @exception PublishingException
859 * if an error occurs
860 */
861 protected WebResponse getAdminPage(final WebResponse summaryPage)
862 throws PublishingException {
863 assertNotNull(summaryPage, "summaryPage");
864
865 try {
866 final WebLink[] links = summaryPage.getLinks();
867 assertArrayFull(links, "summaryPage.getLinks()");
868
869 final WebLink adminLink = this.findLinkWithExactText(links, "Admin");
870 assertNotNull(adminLink, "adminLink");
871
872 final WebResponse returnMe = adminLink.click();
873 assertNotNull(returnMe, "returnMe");
874
875 return returnMe;
876
877 } catch (final IOException wrapMe) {
878 throw new PublishingException(wrapMe);
879 } catch (final SAXException wrapMe) {
880 throw new PublishingException(wrapMe);
881 }
882 }
883
884 /***
885 * Returns a {@link WebResponse} that represents the "packages page" for the
886 * <a href="http://sourceforge.net/">SourceForge</a> project summarized in the
887 * supplied {@link WebResponse} that must have been supplied by the {@link
888 * #getAdminPage(WebResponse)} method. This method will never return
889 * <code>null</code>.
890 *
891 * @param adminPage
892 * the {@link WebResponse} returned by the {@link
893 * #getAdminPage(WebResponse)} method; must not be
894 * <code>null</code>
895 * @return a {@link WebResponse} that represents the "packages page" for
896 * the <a href="http://sourceforge.net/">SourceForge</a> project
897 * summarized in the supplied {@link WebResponse}; never
898 * <code>null</code>
899 * @exception PublishingException
900 * if an error occurs
901 */
902 protected WebResponse getPackagesPage(final WebResponse adminPage)
903 throws PublishingException {
904 assertNotNull(adminPage, "adminPage");
905
906 try {
907 final WebLink fileReleasesLink =
908 adminPage.getLinkWith("File Releases");
909 assertNotNull(fileReleasesLink, "fileReleasesLink");
910
911 final WebResponse packagesPage = fileReleasesLink/click()/package-summary.html">trong> WebResponse packagesPage = fileReleasesLink.click();
912 assertNotNull(packagesPage, "packagesPage");
913
914 return packagesPage/package-summary.html">trong> packagesPage;
915
916 } catch (final IOException wrapMe) {
917 throw new PublishingException(wrapMe);
918 } catch (final SAXException wrapMe) {
919 throw new PublishingException(wrapMe);
920 }
921 }
922
923 /***
924 * Logs the supplied {@link Project}'s {@link Project#getAdministrator()
925 * Administrator} into the supplied {@link Project} on <a
926 * href="http://sourceforge.net/">SourceForge</a>. The login lasts for as
927 * long as the supplied {@link WebConversation} is in memory.
928 *
929 * <p>This method never returns <code>null</code>.</p>
930 *
931 * @param conversation
932 * the {@link WebConversation} in whose scope the login will
933 * take place; must not be <code>null</code>
934 * @param project
935 * the {@link Project} to log into; also the source of the
936 * {@link Administrator} who will be logged in; must not be
937 * <code>null</code> and must provide a non-<code>null</code>
938 * {@link Project#getAdministrator() Administrator}
939 * @return the {@link WebResponse} corresponding to the page that is shown
940 * once a login has completed successfully; never
941 * <code>null</code>
942 * @exception InvalidCredentialsException
943 * if the associated {@link Administrator} could not log in
944 * @exception PublishingException
945 * if any other error occurs
946 */
947 protected WebResponse login(final WebConversation conversation,
948 final Project project)
949 throws InvalidCredentialsException, PublishingException {
950 assertNotNull(conversation, "conversation");
951 assertNotNull(project, "project");
952 final Administrator administrator = project.getAdministrator();
953 assertNotNull(administrator, "administrator");
954 final String userName = administrator.getName();
955 assertNotNull(userName, "userName");
956 final String password = administrator.getPassword();
957
958 try {
959 final WebResponse getLoginPageResponse =
960 conversation.getResponse(new GetMethodWebRequest(LOGIN_URL));
961 assertNotNull(getLoginPageResponse, "getLoginPageResponse");
962 LOGGER.info("Retrieved " + getLoginPageResponse.getTitle());
963
964 final WebForm[] forms = getLoginPageResponse.getForms();
965 assertArrayFull(forms, "forms");
966
967 final WebForm loginForm =
968 this.findFormWithAction(forms,
969 "https://sourceforge.net/account/login.php");
970 assertNotNull(loginForm, "loginForm");
971
972 loginForm.setParameter("form_loginname", userName);
973 loginForm.setParameter("form_pw", password);
974 loginForm.removeParameter("stay_in_ssl");
975 loginForm.removeParameter("persistent_login");
976
977 final WebRequest loginSubmissionRequest = loginForm.getRequest("login");
978 assertNotNull(loginSubmissionRequest, "loginSubmissionRequest");
979
980 final WebResponse loginResponse =
981 conversation.getResponse(loginSubmissionRequest);
982 assertNotNull(loginResponse, "loginResponse");
983 LOGGER.info("Retrieved " + loginResponse.getTitle());
984
985 final String text = loginResponse.getText();
986 assertNotNull(text, "text");
987
988 if (text.indexOf("Invalid Password or User Name") >= 0) {
989 throw new InvalidCredentialsException(userName, password);
990 }
991
992 return loginResponse;
993
994 } catch (final IOException wrapMe) {
995 throw new PublishingException(wrapMe);
996 } catch (final SAXException wrapMe) {
997 throw new PublishingException(wrapMe);
998 }
999 }
1000
1001 /***
1002 * A convenience method that extracts a {@link WebLink} from the supplied
1003 * array of {@link WebLink}s provided that its {@linkplain WebLink#asText()
1004 * text} is equal to the supplied {@link String}. This method may return
1005 * <code>null</code>.
1006 *
1007 * @param links
1008 * the array of {@link WebLink}s from which an appropriate
1009 * {@link WebLink} is to be extracted; may be <code>null</code>
1010 * in which case <code>null</code> will be returned
1011 * @param textToMatch
1012 * the {@link String} to which a {@link WebLink}'s {@linkplain
1013 * WebLink#asText() text} must be equal in order to be selected;
1014 * if <code>null</code>, then <code>null</code> will be returned
1015 * @return a {@link WebLink} from the supplied array of {@link WebLink}s
1016 * provided that its {@linkplain WebLink#asText() text} is equal
1017 * to the supplied {@link String}, or <code>null</code>
1018 */
1019 protected WebLink findLinkWithExactText(final WebLink[] links,
1020 final String textToMatch) {
1021 if (textToMatch == null || links == null || links.length <= 0) {
1022 return null;
1023 }
1024 WebLink link = null;
1025 String linkText;
1026 boolean found = false;
1027 for (int i = 0; i < links.length; i++) {
1028 link = links[i];
1029 if (link != null) {
1030 linkText = link.asText();
1031 if (linkText != null) {
1032 if (textToMatch.equals(linkText.trim())) {
1033 found = true;
1034 break;
1035 }
1036 }
1037 }
1038 }
1039 if (!found) {
1040 return null;
1041 }
1042 assert link != null;
1043 return link;
1044 }
1045
1046 /***
1047 * Extracts a {@link WebForm} from the supplied array of {@link WebForm}s,
1048 * provided that its {@linkplain WebForm#getAction() action} is equal to the
1049 * supplied {@link String}. This method may return <code>null</code>.
1050 *
1051 * @param forms
1052 * the array of {@link WebForm}s to consider; if
1053 * <code>null</code> then <code>null</code> will be returned
1054 * @param action
1055 * the action {@link String} to which a given {@link WebForm}'s
1056 * {@linkplain WebForm#getAction() action} must be equal in
1057 * order for that {@link WebForm} to be returned; if
1058 * <code>null</code> then <code>null</code> will be returned
1059 * @return a {@link WebForm} from the supplied array of {@link WebForm}s,
1060 * provided that its {@linkplain WebForm#getAction() action} is
1061 * equal to the supplied {@link String}, or <code>null</code>
1062 */
1063 protected WebForm findFormWithAction(final WebForm[] forms,
1064 final String action) {
1065 if (forms == null || forms.length <= 0) {
1066 return null;
1067 }
1068 WebForm form;
1069 String formAction;
1070 for (int i = 0; i < forms.length; i++) {
1071 form = forms[i];
1072 if (form != null) {
1073 formAction = form.getAction();
1074 if (formAction == null) {
1075 if (action == null) {
1076 return null;
1077 }
1078 } else {
1079 if (formAction.equals(action)) {
1080 return form;
1081 }
1082 }
1083 }
1084 }
1085 return null;
1086 }
1087
1088 /***
1089 * Returns an array of {@link WebForm}s whose elements are those {@link
1090 * WebForm}s extracted from the supplied {@link WebForm} array with
1091 * {@linkplain WebForm#getAction() actions} equal to the supplied {@link
1092 * String}. This method never returns <code>null</code>.
1093 *
1094 * @param forms
1095 * the {@link WebForm} array in which to find candidates; may
1096 * be <code>null</code>
1097 * @param action
1098 * the {@linkplain WebForm#getAction() action} {@link String} to
1099 * match; may be <code>null</code>
1100 * @return an array of {@link WebForm}s whose elements are those {@link
1101 * WebForm}s extracted from the supplied {@link WebForm} array
1102 * with {@linkplain WebForm#getAction() actions} equal to the
1103 * supplied {@link String}; never <code>null</code>
1104 */
1105 protected WebForm[] retainFormsWithAction(final WebForm[] forms,
1106 final String action) {
1107 if (forms == null || forms.length <= 0) {
1108 return new WebForm[0];
1109 }
1110 final Collection returnForms = new ArrayList(forms.length);
1111 WebForm form;
1112 String formAction;
1113 for (int i = 0; i < forms.length; i++) {
1114 form = forms[i];
1115 if (form != null) {
1116 formAction = form.getAction();
1117 if (formAction == null) {
1118 if (action == null) {
1119 returnForms.add(form);
1120 }
1121 } else {
1122 if (formAction.equals(action)) {
1123 returnForms.add(form);
1124 }
1125 }
1126 }
1127 }
1128 return (WebForm[])returnForms.toArray(new WebForm[returnForms.size()]);
1129 }
1130
1131 /***
1132 * Creates a new package on the <a
1133 * href="http://sourceforge.net/">SourceForge</a> website and returns the
1134 * resulting "packages page", which should now include the newly-created
1135 * package among its list of existing packages. This method may return
1136 * <code>null</code>, or at least cannot be guaranteed to return a
1137 * non-<code>null</code> result.
1138 *
1139 * <p>This method is called from the {@link
1140 * #getEditReleasePage(WebConversation, WebResponse, FileRelease)} method.</p>
1141 *
1142 * @param createPackageForm
1143 * a {@link WebForm} that represents the "create package" form
1144 * on the "packages page"; must not be <code>null</code>
1145 * @param packageToCreate
1146 * the {@link Package} to be created on <a
1147 * href="http://sourceforge.net/">SourceForge</a>; must not be
1148 * <code>null</code>
1149 * @return a {@link WebResponse} representing the "packages page", or
1150 * <code>null</code>
1151 * @exception PublishingException
1152 * if an error occurs
1153 */
1154 protected WebResponse createPackage(final WebForm createPackageForm,
1155 Package packageToCreate)/package-summary.html">final Package packageToCreate)
1156 throws PublishingException {
1157 assertNotNull(createPackageForm, "createPackageForm");
1158 assertNotNull(packageToCreate, "packageToCreate");
1159 String packageName = packageToCreate.getName();
1160 assertNotNull(packageName, "packageName");
1161
1162 try {
1163 if (!EDIT_PACKAGES_ACTION.equals(createPackageForm.getAction()) ||
1164 !ADD_PACKAGE.equals(createPackageForm.getParameterValue(PACKAGE_ACTION_PARAMETER))) {
1165 throw new PublishingException("Wrong form");
1166 }
1167 createPackageForm.setParameter(PACKAGE_NAME, packageName);
1168 return createPackageForm.submit();
1169 } catch (final IOException kaboom) {
1170 throw new PublishingException(kaboom);
1171 } catch (final SAXException kaboom) {
1172 throw new PublishingException(kaboom);
1173 }
1174 }
1175
1176 /***
1177 * Returns a {@link WebResponse} representing the "edit release" page.
1178 * Producing this page may involve creating a new file release or a new
1179 * package along the way to accurately reflect the contents of the supplied
1180 * {@link FileRelease}. This method never returns <code>null</code>.
1181 *
1182 * @param conversation
1183 * the {@link WebConversation} currently in effect; must not be
1184 * <code>null</code>
1185 * @param packagesPage
1186 * a {@link WebResponse} representing the "packages page"; must
1187 * not be <code>null</code>
1188 * @param fileRelease
1189 * the {@link FileRelease} whose <a
1190 * href="http://sourceforge.net/">SourceForge</a> analog is to
1191 * be edited; must not be <code>null</code>
1192 * @return a {@link WebResponse} representing the "edit release" page;
1193 * never <code>null</code>
1194 * @exception PublishingException
1195 * if an error occurs
1196 */
1197 protected WebResponse getEditReleasePage(final WebConversation conversation,
1198 WebResponse packagesPage,
1199 final FileRelease fileRelease)
1200 throws PublishingException {
1201 assertNotNull(conversation, "conversation");
1202 assertNotNull(packagesPage, "packagesPage");
1203 assertNotNull(fileRelease, "fileRelease");
1204 final Package pkg = fileRelease.getPackage();
1205 assertNotNull(pkg, "pkg");
1206 final Project project = pkg.getProject();
1207 assertNotNull(project, "project");
1208 final String fileReleaseName = fileRelease.getName();
1209
1210 try {
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234 final WebForm[] packageForms =/package-summary.html">trong> WebForm[] packageForms =
1235 this.retainFormsWithAction(packagesPage.getForms(),
1236 EDIT_PACKAGES_ACTION);
1237 assertArrayFull(packageForms, "packageForms");
1238
1239 if (packageForms/length == 1) {/package-summary.html">trong> (packageForms.length == 1) {
1240
1241
1242
1243
1244
1245
1246
1247 return
1248 this.getEditReleasePage(conversation,
1249 this.createPackage(packageForms[0],
1250 pkg),
1251 fileRelease);
1252 }
1253
1254
1255
1256
1257
1258 WebForm createPackageForm = null;
1259 WebForm updatePackageForm = null;
1260 WebForm currentPackageForm;
1261 for (int i = 0; i < packageForms.length; i++) {
1262 currentPackageForm = packageForms[i];
1263 if (this.isUpdatePackageFormFor(currentPackageForm, pkg)) {
1264 updatePackageForm = currentPackageForm;
1265 break;
1266 } else if (this.isAddPackageForm(currentPackageForm)) {
1267 createPackageForm = currentPackageForm;
1268
1269
1270 }
1271 }
1272 if (updatePackageForm == null) {
1273 assertNotNull(createPackageForm, "createPackageForm");
1274
1275
1276
1277
1278
1279 return
1280 this.getEditReleasePage(conversation,
1281 this.createPackage(createPackageForm,
1282 pkg),
1283 fileRelease);
1284 }
1285
1286
1287
1288
1289 packagesPage = this.synchronizeStatus(updatePackageForm, pkg);
1290
1291
1292
1293
1294
1295
1296
1297
1298 final String packageID = updatePackageForm/getParameterValue(PACKAGE_ID)/package-summary.html">trong> String packageID = updatePackageForm.getParameterValue(PACKAGE_ID);
1299 assertNotNull(packageID, "packageID");
1300
1301 pkg.setID(packageID);
1302
1303
1304
1305 final String groupID = updatePackageForm.getParameterValue(GROUP_ID);
1306 assertNotNull(groupID, "groupID");
1307
1308 project.setID(groupID);
1309
1310 final WebResponse releasesPage =
1311 this.getReleasesPage(conversation, packageID, groupID);
1312
1313 if (releasesPage != null) {
1314
1315
1316 LOGGER.info("Found releases page for " + pkg.getName());
1317 final WebLink editThisReleaseLink =
1318 this.getEditReleaseLink(releasesPage, fileRelease);
1319 if (editThisReleaseLink != null) {
1320 return editThisReleaseLink.click();
1321 }
1322 }
1323
1324
1325
1326
1327
1328
1329 final String newReleaseURL =
1330 this.buildNewReleaseHref(conversation, packageID, groupID);
1331 assertNotNull(newReleaseURL, "newReleaseURL");
1332
1333 final WebResponse createReleasePage =
1334 conversation.getResponse(newReleaseURL);
1335 assertNotNull(createReleasePage, "createReleasePage");
1336 LOGGER.info("Retrieved " + createReleasePage.getTitle());
1337
1338 final WebForm newReleaseForm =
1339 this.findFormWithAction(createReleasePage.getForms(),
1340 "/project/admin/newrelease.php");
1341 assertNotNull(newReleaseForm, "newReleaseForm");
1342
1343 newReleaseForm.setParameter(RELEASE_NAME, fileRelease.getName());
1344 newReleaseForm.setParameter(PACKAGE_ID, packageID);
1345 newReleaseForm.setParameter(GROUP_ID, groupID);
1346
1347 return newReleaseForm.submit();
1348
1349 } catch (final IOException kaboom) {
1350 throw new PublishingException(kaboom);
1351 } catch (final SAXException kaboom) {
1352 throw new PublishingException(kaboom);
1353 }
1354 }
1355
1356 /***
1357 * Changes the {@linkplain HideableNamedObject#isHidden() hidden status} of
1358 * the <a href="http://sourceforge.net/'>SourceForge</a> analog of the
1359 * supplied {@link HideableNamedObject} to match that of the supplied {@link
1360 * HideableNamedObject}. This method is called from the {@link
1361 * #getEditReleasePage(WebConversation, WebResponse, FileRelease)} method and
1362 * never returns <code>null</code>.
1363 *
1364 * @param updateForm
1365 * a {@link WebForm} capable of updating the hidden status of
1366 * either a file release or a package (or another type of object
1367 * that can be hidden); must not be <code>null</code>
1368 * @param hideable
1369 * a {@link HideableNamedObject} whose {@linkplain
1370 * HideableNamedObject#isHidden() hidden status} will be
1371 * examined; must not be <code>null</code>
1372 * @return the result of {@linkplain WebForm#submit() submitting} the
1373 * the supplied {@link WebForm}; never <code>null</code>
1374 * @exception PublishingException
1375 * if an error occurs
1376 */
1377 protected WebResponse synchronizeStatus(final WebForm updateForm,
1378 final HideableNamedObject hideable)
1379 throws PublishingException {
1380 assertNotNull(updateForm, "updateForm");
1381 assertNotNull(hideable, "hideable");
1382 final String name = hideable.getName();
1383 assertNotNull(name, "name");
1384
1385 try {
1386
1387
1388
1389
1390 final String status = updateForm.getParameterValue(STATUS);
1391 assertNotNull(status, "status");
1392
1393 WebResponse currentPage = null;
1394 if (VISIBLE.equals(status) && hideable.isHidden()) {
1395 updateForm.setParameter(STATUS, HIDDEN);
1396 LOGGER.info("Changing status for " + name + " to hidden");
1397 currentPage = updateForm.submit();
1398 } else if (HIDDEN.equals(status) && !hideable.isHidden()) {
1399 updateForm.setParameter(STATUS, VISIBLE);
1400 LOGGER.info("Changing status for " + name + " to visible");
1401 currentPage = updateForm.submit();
1402 }
1403
1404 return currentPage;
1405
1406 } catch (final IOException kaboom) {
1407 throw new PublishingException(kaboom);
1408 } catch (final SAXException kaboom) {
1409 throw new PublishingException(kaboom);
1410 }
1411 }
1412
1413 /***
1414 * Returns <code>true</code> if the supplied {@link WebForm} is one that can
1415 * add a package to <a href="http://sourceforge.net/">SourceForge</a>.
1416 *
1417 * @param packageForm
1418 * the {@link WebForm} to test; if <code>null</code> then
1419 * <code>false</code> will be returned
1420 * @return <code>true</code> if the supplied {@link WebForm} can
1421 * add a package to <a
1422 * href="http://sourceforge.net/">SourceForge</a>
1423 */
1424 protected boolean isAddPackageForm(final WebForm packageForm) {/package-summary.html">g> boolean isAddPackageForm(final WebForm packageForm) {
1425 return this/isPackageFormWithFunction(packageForm, ADD_PACKAGE)/package-summary.html">ong> this.isPackageFormWithFunction(packageForm, ADD_PACKAGE);
1426 }
1427
1428 /***
1429 * Returns <code>true</code> if the supplied {@link WebForm} is capable of
1430 * updating the <a href="http://sourceforge.net/">SourceForge</a> analog of
1431 * the supplied {@link Package}.
1432 *
1433 * @param packageForm
1434 * the {@link WebForm} to test; if <code>null</code> then
1435 * <code>false</code> will be returned
1436 * @param pkg
1437 * the {@link Package} in question; if <code>null</code> then
1438 * <code>false</code> will be returned
1439 * @return <code>true</code> if the supplied {@link WebForm} is capable of
1440 * editing the <a href="http://sourceforge.net/">SourceForge</a>
1441 * analog of the supplied {@link Package}
1442 */
1443 protected boolean isUpdatePackageFormFor(final WebForm packageForm,/package-summary.html">g> boolean isUpdatePackageFormFor(final WebForm packageForm,
1444 final Package pkg) {
1445 return
1446 packageForm != null &&
1447 pkg != null &&
1448 this.isPackageFormFor(packageForm, pkg) &&
1449 this.isPackageFormWithFunction(packageForm, UPDATE_PACKAGE);
1450 }
1451
1452 /***
1453 * Returns <code>true</code> if the supplied {@link WebForm} will affect the
1454 * <a href="http://sourceforge.net/">SourceForge</a> analog of the supplied
1455 * {@link Package} in some way.
1456 *
1457 * <p>This method is called only by the {@link
1458 * #isUpdatePackageFormFor(WebForm, Package)} method.</p>
1459 *
1460 * @param packageForm
1461 * the {@link WebForm} to test; if <code>null</code> then
1462 * <code>false</code> will be returned
1463 * @param pkg
1464 * the {@link Package} in question; if <code>null</code> then
1465 * <code>false</code> will be returned
1466 * @return <code>true</code> if the supplied {@link WebForm} will affect
1467 * the <a href="http://sourceforge.net/">SourceForge</a>
1468 * analog of the supplied {@link Package} in some way
1469 */
1470 protected boolean isPackageFormFor(final WebForm packageForm,/package-summary.html">g> boolean isPackageFormFor(final WebForm packageForm,
1471 final Package pkg) {
1472 if (packageForm == null || pkg == null) {/package-summary.html">ong> (packageForm == null || pkg == null) {
1473 return false;
1474 }
1475 final String pkgName = pkg.getName();
1476 final String packageName =/package-summary.html">ong> String packageName =
1477 packageForm.getParameterValue(PACKAGE_NAME);
1478 if (pkgName == null) {
1479 return packageName == null/package-summary.html">trong> packageName == null;
1480 }
1481 return pkgName/equals(packageName)/package-summary.html">ong> pkgName.equals(packageName);
1482 }
1483
1484 /***
1485 * Returns <code>true</code> if the supplied {@link WebForm} represents a form
1486 * to edit a package that has the supplied {@link String} as the value of its
1487 * "<code>func</code>" input parameter. This method is essentially a
1488 * refactored helper method that is called by the {@link
1489 * #isUpdatePackageFormFor(WebForm, Package)} method.
1490 *
1491 * @param packageForm
1492 * the {@link WebForm} to test; may be <code>null</code> in
1493 * which case <code>false</code> is returned
1494 * @param soughtValue
1495 * the "<code>func</code>" parameter value to look for; may be
1496 * <code>null</code> in which case this method will almost
1497 * certainly return <code>false</code> given that it's extremely
1498 * unlikely that the SourceForge website will have
1499 * "<code>null</code>" or "" as the value of this parameter
1500 * @return <code>true</code> if the supplied {@link WebForm} represents a
1501 * form to edit a package that has the supplied {@link String}
1502 * as the value of its "<code>func</code>" input parameter;
1503 * <code>false</code> in absolutely all other cases
1504 */
1505 protected boolean isPackageFormWithFunction(final WebForm packageForm,/package-summary.html">g> boolean isPackageFormWithFunction(final WebForm packageForm,
1506 final String soughtValue) {
1507 if (packageForm == null) {/package-summary.html">ong> (packageForm == null) {
1508 return false;
1509 }
1510 final String packageAction =/package-summary.html">ong> String packageAction =
1511 packageForm.getParameterValue(PACKAGE_ACTION_PARAMETER);
1512 if (soughtValue == null) {
1513 return packageAction == null/package-summary.html">trong> packageAction == null;
1514 }
1515 return soughtValue/equals(packageAction)/package-summary.html">ong> soughtValue.equals(packageAction);
1516 }
1517
1518 /***
1519 * Returns <code>true</code> if the supplied {@link WebResponse} represents
1520 * the "edit releases" page.
1521 *
1522 * @param page
1523 * the {@link WebResponse} to test; must not be
1524 * <code>null</code>
1525 * @param checkForReleases
1526 * whether or not to dig a little deeper and see if there are
1527 * file releases on the page represented by the supplied {@link
1528 * WebResponse}
1529 * @return <code>true</code> if the supplied {@link WebResponse}
1530 * represents the "edit releases" page
1531 * @exception PublishingException
1532 * if an error occurs
1533 */
1534 protected boolean isReleasesPage(final WebResponse page,
1535 final boolean checkForReleases)
1536 throws PublishingException {
1537 assertNotNull(page, "page");
1538
1539 try {
1540 final String title = page.getTitle();
1541 if (title == null || title.indexOf("FRS: Releases") < 0) {
1542 return false;
1543 }
1544 if (checkForReleases) {
1545 final String text = page.getText();
1546 return
1547 text != null &&
1548 text.indexOf("You Have No Releases Of This Package Defined") < 0;
1549 }
1550 return true;
1551 } catch (final IOException kaboom) {
1552 throw new PublishingException(kaboom);
1553 } catch (final SAXException kaboom) {
1554 throw new PublishingException(kaboom);
1555 }
1556 }
1557
1558 /***
1559 * Returns a {@link String} that, when treated as a URL, will edit the file
1560 * releases that belong to the package and project with the corresponding
1561 * supplied identifiers. This method never returns <code>null</code>.
1562 *
1563 * @param conversation
1564 * the {@link WebConversation} currently in effect; must not be
1565 * <code>null</code>
1566 * @param packageID
1567 * the identifier of the package whose file releases will
1568 * ultimately be edited; must not be <code>null</code>
1569 * @param groupID
1570 * the identifier of the project whose file releases will
1571 * ultimately be edited; must not be <code>null</code> and must
1572 * have a package with the supplied package identifier
1573 * @return a {@link String} that, when treated as a URL, will edit the
1574 * file releases that belong to the package and project with the
1575 * corresponding supplied identifiers; never <code>null</code>
1576 * @exception PublishingException
1577 * if an error occurs
1578 */
1579 protected String buildEditReleasesHref(final WebConversation conversation,
1580 final String packageID,
1581 final String groupID)
1582 throws PublishingException {
1583 assertNotNull(conversation, "conversation");
1584 assertNotNull(packageID, "packageID");
1585 assertNotNull(groupID, "groupID");
1586
1587 final WebResponse currentPage = conversation.getCurrentPage();
1588 assertNotNull(currentPage, "currentPage");
1589
1590 final URL currentPageURL = currentPage.getURL();
1591 assertNotNull(currentPageURL, "currentPageURL");
1592
1593 final StringBuffer returnMe =
1594 new StringBuffer("editreleases.php?package_id=");
1595 returnMe.append(packageID);
1596 returnMe.append("&group_id=");
1597 returnMe.append(groupID);
1598 try {
1599 return new URL(currentPageURL, returnMe.toString()).toString();
1600 } catch (final MalformedURLException kaboom) {
1601 throw new PublishingException(kaboom);
1602 }
1603 }
1604
1605 /***
1606 * Returns a {@link String} that, when treated as a URL, will create a new
1607 * file release that will belong to the package and project with the
1608 * corresponding supplied identifiers. This method never returns
1609 * <code>null</code>.
1610 *
1611 * @param conversation
1612 * the {@link WebConversation} currently in effect; must not be
1613 * <code>null</code>
1614 * @param packageID
1615 * the identifier of the package to which a new file release
1616 * will belong; must not be <code>null</code>
1617 * @param groupID
1618 * the identifier of the project to which a new file release
1619 * will belong; must not be <code>null</code> and must have a
1620 * package with the supplied package identifier
1621 * @return a {@link String} that, when treated as a URL, will create a new
1622 * file release that will belong to the package and project with
1623 * the corresponding supplied identifiers; never
1624 * <code>null</code>
1625 * @exception PublishingException
1626 * if an error occurs
1627 */
1628 protected String buildNewReleaseHref(final WebConversation conversation,
1629 final String packageID,
1630 final String groupID)
1631 throws PublishingException {
1632 assertNotNull(conversation, "conversation");
1633 assertNotNull(packageID, "packageID");
1634 assertNotNull(groupID, "groupID");
1635
1636 final WebResponse currentPage = conversation.getCurrentPage();
1637 assertNotNull(currentPage, "currentPage");
1638
1639 final URL currentPageURL = currentPage.getURL();
1640 assertNotNull(currentPageURL, "currentPageURL");
1641
1642 final StringBuffer returnMe =
1643 new StringBuffer("newrelease.php?package_id=");
1644 returnMe.append(packageID);
1645 returnMe.append("&group_id=");
1646 returnMe.append(groupID);
1647 try {
1648 return new URL(currentPageURL, returnMe.toString()).toString();
1649 } catch (final MalformedURLException kaboom) {
1650 throw new PublishingException(kaboom);
1651 }
1652 }
1653
1654 /***
1655 * Returns a {@link WebResponse} that represents the "releases page"
1656 * appropriate for the given project and package identifiers. This method may
1657 * return <code>null</code>.
1658 *
1659 * @param conversation
1660 * the {@link WebConversation} currently in effect; must not be
1661 * <code>null</code>
1662 * @param packageID
1663 * the identifier of the package whose releases will be shown;
1664 * must not be <code>null</code>
1665 * @param groupID
1666 * the identifier of the project to which the package with the
1667 * supplied package identifier must belong; must not be
1668 * <code>null</code>
1669 * @return a {@link WebResponse} that represents the "releases page", or
1670 * <code>null</code>
1671 * @exception PublishingException
1672 * if an error occurs
1673 */
1674 protected WebResponse getReleasesPage(final WebConversation conversation,
1675 final String packageID,
1676 final String groupID)
1677 throws PublishingException {
1678 assertNotNull(conversation, "conversation");
1679 assertNotNull(packageID, "packageID");
1680 assertNotNull(groupID, "groupID");
1681
1682 try {
1683 final String url =
1684 this.buildEditReleasesHref(conversation, packageID, groupID);
1685 assertNotNull(url, "url");
1686
1687 final WebResponse editReleasesPage = conversation.getResponse(url);
1688 assertNotNull(editReleasesPage, "editReleasesPage");
1689 LOGGER.info("Retrieved " + editReleasesPage.getTitle());
1690
1691 if (this.isReleasesPage(editReleasesPage, true)) {
1692 return editReleasesPage;
1693 }
1694 return null;
1695 } catch (final IOException kaboom) {
1696 throw new PublishingException(kaboom);
1697 } catch (final SAXException kaboom) {
1698 throw new PublishingException(kaboom);
1699 }
1700 }
1701
1702 /***
1703 * A very special-purpose helper method that extracts the "<code>[Edit
1704 * Release]</code>" link from the "releases page" that is appropriate for the
1705 * supplied {@link FileRelease}. This method may return <code>null</code>.
1706 *
1707 * @param releasesPage
1708 * a {@link WebResponse} representing the "releases page"; must
1709 * not be <code>null</code>
1710 * @param release
1711 * the {@link FileRelease} in question; must not be
1712 * <code>null</code>
1713 * @return a {@link WebLink} corresponding to the appropriate "<code>[Edit
1714 * Release]</code>" link, or <code>null</code>
1715 * @exception PublishingException
1716 * if an error occurs
1717 */
1718 protected WebLink getEditReleaseLink(final WebResponse releasesPage,
1719 final FileRelease release)
1720 throws PublishingException {
1721 assertNotNull(releasesPage, "releasesPage");
1722 assertNotNull(release, "release");
1723 try {
1724
1725
1726
1727
1728
1729 final WebTable table = releasesPage.getTableStartingWith("Release Name");
1730 assertNotNull(table, "table");
1731
1732 final int rowCount = table.getRowCount();
1733 TableCell cell;
1734 String cellText;
1735 String releaseName;
1736 int leftBracketIndex;
1737 WebLink link;
1738 for (int i = 1; i < rowCount; i++) {
1739 cell = table.getTableCell(i, 0);
1740 if (cell != null) {
1741 cellText = cell.asText();
1742 if (cellText != null) {
1743 leftBracketIndex = cellText.indexOf("[Edit This Release]");
1744 if (leftBracketIndex > 0) {
1745 releaseName = cellText.substring(0, leftBracketIndex);
1746 if (releaseName != null) {
1747 releaseName = releaseName.trim();
1748 if (releaseName != null &&
1749 releaseName.equals(release.getName())) {
1750 link = cell.getLinkWith("[Edit This Release]");
1751 if (link != null) {
1752 return link;
1753 }
1754 }
1755 }
1756 }
1757 }
1758 }
1759 }
1760 return null;
1761 } catch (final SAXException kaboom) {
1762 throw new PublishingException(kaboom);
1763 }
1764 }
1765
1766 /***
1767 * A convenience method that throws a {@link NullObjectException} if the
1768 * supplied {@link Object} is <code>null</code>. This method is primarily
1769 * used to check method arguments and in other cases where a checked {@link
1770 * Exception} is preferred to the usual {@link IllegalArgumentException}
1771 * alternative.
1772 *
1773 * @param object
1774 * the {@link Object} to check; if <code>null</code> a {@link
1775 * NullObjectException} will be thrown
1776 * @param message
1777 * a message describing the problem if the supplied {@link
1778 * Object} is <code>null</code>; may be <code>null</code>
1779 * @exception NullObjectException
1780 * if the supplied {@link Object} is <code>null</code>
1781 */
1782 protected static final void assertNotNull(final Object object,
1783 final String message)
1784 throws NullObjectException {
1785 if (object == null) {
1786 throw new NullObjectException(message);
1787 }
1788 }
1789
1790 /***
1791 * A convenience metohd that throws either a {@link NullObjectException} or an
1792 * {@link EmptyArrayException} if the supplied {@link Object} array is
1793 * <code>null</code> or has a length less than or equal to <code>0</code>
1794 * respectively.
1795 *
1796 * @param object
1797 * the {@link Object} array to check; if <code>null</code> a
1798 * {@link NullObjectException} will be thrown and if empty an
1799 * {@link EmptyArrayException} will be thrown
1800 * @param message
1801 * a message describing the problem if the supplied {@link
1802 * Object} array is <code>null</code> or empty; may be
1803 * <code>null</code>
1804 * @exception NullObjectException
1805 * if the supplied {@link Object} array is <code>null</code>
1806 * @exception EmptyArrayException
1807 * if the supplied {@link Object} array has a length less than
1808 * or equal to <code>0</code>
1809 */
1810 protected static final void assertArrayFull(final Object[] object,
1811 final String message)
1812 throws NullObjectException, EmptyArrayException {
1813 assertNotNull(object, message);
1814 if (object.length <= 0) {
1815 throw new EmptyArrayException(message);
1816 }
1817 }
1818
1819 /***
1820 * A {@link Thread} that uploads a {@link File} to an FTP site.
1821 *
1822 * @author <a href="mailto:ljnelson94@alumni.amherst.edu">Laird Nelson</a>
1823 * @version $Revision: 1.15 $ $Date: 2004/07/27 20:20:34 $
1824 * @since June 19, 2003 */
1825 static final class FileUploader extends Thread {
1826
1827 /***
1828 * The FTP URL to which this {@link HttpUnitPublisher.FileUploader}'s
1829 * {@linkplain #getFile() associated <code>File</code>} will be uploaded.
1830 */
1831 private static final String UPLOAD_DIRECTORY =
1832 "ftp://upload.sourceforge.net/incoming";
1833
1834 /***
1835 * The {@link File} to upload. This field may be <code>null</code>.
1836 *
1837 * @see #getFile()
1838 */
1839 private final File file;
1840
1841 /***
1842 * The {@link Collection} to which any {@link Exception}s encountered during
1843 * the execution of the {@link #upload()} method will be added. This field
1844 * must never be <code>null</code>.
1845 *
1846 * @see #getErrors()
1847 */
1848 private final Collection errors;
1849
1850 /***
1851 * Creates a new {@link HttpUnitPublisher.FileUploader}.
1852 *
1853 * @param file
1854 * the {@link File} to upload; may be <code>null</code> in
1855 * which case the {@link #upload()} method will do nothing
1856 * @param errors
1857 * the {@link Collection} to which any {@link Exception}s
1858 * encountered during the execution of the {@link #upload()}
1859 * method will be added; may be <code>null</code> in which
1860 * case a new {@link ArrayList} will be used instead
1861 */
1862 FileUploader(final File file, final Collection errors) {
1863 super();
1864 this.file = file;
1865 if (errors == null) {
1866 this.errors = new ArrayList(5);
1867 } else {
1868 this.errors = errors;
1869 }
1870 }
1871
1872 /***
1873 * Returns the {@link File} to upload. This method may return
1874 * <code>null</code>.
1875 *
1876 * @return the {@link File} to upload, or <code>null</code>
1877 */
1878 public File getFile() {
1879 return this.file;
1880 }
1881
1882 /***
1883 * Returns a {@link Collection} of any errors encountered during the
1884 * execution of either the {@link #run()} or the {@link #upload()} method.
1885 * This method may return <code>null</code>.
1886 *
1887 * @return a {@link Collection} of errors, or <code>null</code>
1888 */
1889 public final Collection getErrors() {
1890 return this.errors;
1891 }
1892
1893 /***
1894 * Calls the {@link #upload()} method.
1895 */
1896 public final void run() {
1897 this.upload();
1898 }
1899
1900 /***
1901 * Uploads this {@link HttpUnitPublisher.FileUploader}'s associated {@link
1902 * #getFile() File} via FTP to the
1903 * <code>ftp://upload.sourceforge.net/incoming/</code> directory. Any
1904 * errors encountered are placed in the {@linkplain #getErrors() associated
1905 * errors <code>Collection</code>}.
1906 */
1907 public final void upload() {
1908 InputStream inputStream = null;
1909 OutputStream outputStream = null;
1910 final File file = this.getFile();
1911 if (file == null) {
1912 return;
1913 }
1914 final Collection errors = this.getErrors();
1915 try {
1916
1917
1918 inputStream = new FileInputStream(file);
1919
1920
1921 final String shortName = file.getName();
1922 assert shortName != null;
1923 final URLConnection connection =
1924 new URL(UPLOAD_DIRECTORY + "/" + shortName).openConnection();
1925 assert connection != null;
1926
1927 connection.setDoOutput(true);
1928 connection.connect();
1929
1930 outputStream = connection.getOutputStream();
1931 assert outputStream != null;
1932
1933
1934 this.copyStream(inputStream, outputStream);
1935
1936 } catch (final Exception kaboom) {
1937 if (errors != null) {
1938 errors.add(kaboom);
1939 }
1940 } finally {
1941 try {
1942 if (outputStream != null) {
1943 outputStream.close();
1944 }
1945 } catch (final IOException ignore) {
1946
1947 }
1948 try {
1949 if (inputStream != null) {
1950 inputStream.close();
1951 }
1952 } catch (final IOException ignore) {
1953
1954 }
1955 }
1956 }
1957
1958 /***
1959 * Copies the supplied {@link InputStream} to the supplied {@link
1960 * OutputStream} in chunks of 1,024 bytes.
1961 *
1962 * @param inputStream
1963 * the {@link InputStream} to copy; must not be
1964 * <code>null</code>
1965 * @param outputStream
1966 * the {@link OutputStream} to copy the supplied {@link
1967 * InputStream} to; must not be <code>null</code>
1968 * @exception IOException
1969 * if an input/output error occurs
1970 */
1971 private final void copyStream(final InputStream inputStream,
1972 final OutputStream outputStream)
1973 throws IOException {
1974 assert inputStream != null;
1975 assert outputStream != null;
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 }
1982
1983 }
1984
1985 }