View Javadoc

1   /* -*- mode: JDE; c-basic-offset: 2; indent-tabs-mode: nil -*-
2    *
3    * $Id: SourceForgeClient.java,v 1.6 2004/07/27 20:20:36 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;
29  
30  import java.io.IOException;
31  
32  import java.net.MalformedURLException;
33  
34  import java.util.ArrayList;
35  import java.util.Collection;
36  import java.util.Collections;
37  import java.util.Map;
38  import java.util.WeakHashMap;
39  
40  import com.meterware.httpunit.ClientProperties;
41  import com.meterware.httpunit.GetMethodWebRequest;
42  import com.meterware.httpunit.HttpUnitOptions;
43  import com.meterware.httpunit.WebConversation;
44  import com.meterware.httpunit.WebRequest;
45  import com.meterware.httpunit.WebResponse;
46  import com.meterware.httpunit.WebForm;
47  import com.meterware.httpunit.WebLink;
48  
49  import org.xml.sax.SAXException;
50  
51  public class SourceForgeClient {
52  
53    /***
54     * The {@link String} that represents the URL to use to log in to <a
55     * href="http://sourceforge.net/">SourceForge</a>.
56     */
57    private static final String LOGIN_URL = 
58      "http://sourceforge.net/account/login.php";
59  
60    /***
61     * The leading portion of the URL required to access a <a
62     * href="http://sourceforge.net/">SourceForge</a> project.
63     */
64    private static final String PROJECT_PREFIX = 
65      "http://sourceforge.net/projects/";
66  
67    /***
68     * A {@link Map} of project IDs indexed by project short names.  This field is
69     * thread-safe.
70     */
71    private static final Map PROJECT_ID_MAP = 
72      Collections.synchronizedMap(new WeakHashMap());
73  
74    /***
75     * Static initializer; initializes various class fields.
76     */
77    static {
78      HttpUnitOptions.setExceptionsThrownOnScriptError(false);
79      HttpUnitOptions.setScriptingEnabled(false);
80    }
81  
82    /***
83     * The {@link WebConversation} to use to represent a session with the <a
84     * href="http://sourceforge.net/">SourceForge</a> website.  This field will
85     * never be <code>null</code>.
86     */
87    private final WebConversation wc;
88  
89    private boolean loggedIn;
90  
91    public SourceForgeClient() {
92      super();
93      this.wc = new WebConversation();
94      final ClientProperties clientProperties = this.wc.getClientProperties();
95      if (clientProperties != null) {
96        clientProperties.setAcceptGzip(false);
97      }
98    }
99  
100   public SourceForgeClient(final String user,
101                            final String password)
102     throws InvalidCredentialsException, SourceForgeException {
103     this();
104     this.login(user, password);
105   }
106 
107   protected WebConversation getWebConversation() {
108     return this.wc;
109   }
110 
111   protected WebResponse getCurrentPage() {
112     final WebConversation wc = this.getWebConversation();
113     if (wc != null) {
114       return wc.getCurrentPage();
115     }
116     return null;
117   }
118   
119   public boolean isLoggedIn() {
120     return this.loggedIn;
121   }
122 
123   protected void setLoggedIn(final boolean loggedIn) {
124     this.loggedIn = loggedIn;
125   }
126 
127   public boolean login(final String user,
128                        final String password)
129     throws InvalidCredentialsException, SourceForgeException
130   {
131 
132     // If we're logged in already, don't do it again.
133     if (this.isLoggedIn()) {
134       return false;
135     }
136 
137     // Reject null or empty users, but just null passwords.  We might actually
138     // want to allow null passwords; not sure.
139     if (user == null || 
140         user.trim().equals("") ||
141         password == null) {
142       throw new InvalidCredentialsException(user, password);
143     }
144 
145     // Fire up a "browser".
146     final WebConversation wc = this.getWebConversation();
147     assertNotNull(wc, "wc");
148 
149     // Get the login page, or whatever resides at its address.  We'll validate
150     // it later.
151     WebResponse loginPageResponse = null;
152     try {
153       loginPageResponse =
154         wc.getResponse(new GetMethodWebRequest("http://sourceforge.net/account/login.php"));
155     } catch (final IOException kaboom) {
156       throw new SourceForgeException(kaboom);
157     } catch (final SAXException kaboom) {
158       throw new SourceForgeException(kaboom);
159     }
160     assertNotNull(loginPageResponse, "loginPageResponse");
161 
162     // Try to get forms off the page.  Obviously if there are none, then we
163     // aren't looking at what we think we're looking at.
164     WebForm[] forms = null;
165     try {
166       forms = loginPageResponse.getForms();
167     } catch (final SAXException kaboom) {
168       throw new SourceForgeException(kaboom);
169     }
170     assertArrayFull(forms, "forms");
171 
172     // Find the login form out of all the forms we got back.
173     final WebForm loginForm =
174       this.findFormWithAction(forms,
175                               "https://sourceforge.net/account/login.php");
176     assertNotNull(loginForm, "loginForm");
177 
178     // Set our login name, our password and that we want the login to be
179     // persistent.  It is not necessary to stay in SSL after this initial login
180     // and, moreover, could lead to problems in some JVMs.
181     loginForm.setParameter("form_loginname", user);
182     loginForm.setParameter("form_pw", password);
183     loginForm.removeParameter("stay_in_ssl");
184     loginForm.setParameter("persistent_login", "1");
185 
186     // Submit the form and log in.
187     final WebRequest loginSubmissionRequest = loginForm.getRequest("login");
188     assertNotNull(loginSubmissionRequest, "loginSubmissionRequest");    
189     WebResponse loginResponse = null;
190     try {
191       loginResponse =
192         wc.getResponse(loginSubmissionRequest);
193     } catch (final IOException kaboom) {
194       throw new SourceForgeException(kaboom);
195     } catch (final SAXException kaboom) {
196       throw new SourceForgeException(kaboom);
197     }
198     assertNotNull(loginResponse, "loginResponse");
199 
200     // Make sure we logged in successfully.
201     String text = null;
202     try {
203       text = loginResponse.getText();
204     } catch (final IOException kaboom) {
205       throw new SourceForgeException(kaboom);
206     }
207     if (text == null || text.indexOf("Invalid Password or User Name") >= 0) {
208       throw new InvalidCredentialsException(user, password);
209     }
210 
211     // Everything's good; return appropriately.
212     this.setLoggedIn(true);
213     return true;
214 
215   }
216 
217   /***
218    * Returns the <a href="http://sourceforge.net/">SourceForge</a> project
219    * identifier given a project short name, or <code>-1</code> if the project
220    * does not exist.
221    *
222    * @param      projectShortName
223    *               the name of the project whose identifier should be returned;
224    *               may be <code>null</code> in which case <code>-1</code> will
225    *               be returned
226    * @return     the identifier corresponding to the <a
227    *               href="http://sourceforge.net/">SourceForge</a> project with
228    *               the supplied short name, or <code>-1</code>
229    * @exception  SourceForgeException
230    *               if an error occurs 
231    */
232   public int getProjectID(final String projectShortName) 
233     throws SourceForgeException {
234     if (projectShortName == null) {
235       return -1;
236     }
237 
238     // Look up this project in the cache first.
239     final Integer projectIDInteger = (Integer)PROJECT_ID_MAP.get(projectShortName);
240     if (projectIDInteger != null) {
241       return projectIDInteger.intValue();
242     }
243 
244     // Otherwise, connect to SourceForge.
245     final WebConversation webConversation = this.getWebConversation();
246     assertNotNull(webConversation, "webConversation");
247 
248     // Go hit the main project page
249     // (http://sourceforge.net/projects/projectShortName).
250     WebResponse summaryPage = null;
251     try {
252       summaryPage = 
253         webConversation.getResponse(new GetMethodWebRequest(PROJECT_PREFIX +
254                                                             projectShortName));
255     } catch (final IOException kaboom) {
256       throw new SourceForgeException(kaboom);
257     } catch (final SAXException kaboom) {
258       throw new SourceForgeException(kaboom);
259     }
260     assertNotNull(summaryPage, "summaryPage");
261 
262     // Find a link on that page titled "[View Members]".  We go after the [View
263     // Members] link because it is the least likely to change or be removed
264     // during a UI change.  Really, any link off the main project page that
265     // contains "group_id=123456" would work.
266     WebLink viewMembersLink = null;
267     try {
268       viewMembersLink = summaryPage.getLinkWith("[View Members]");
269     } catch (final SAXException kaboom) {
270       throw new SourceForgeException(kaboom);
271     }
272     if (viewMembersLink == null) {
273       // Couldn't find the [View Members] link.  That can't happen for a valid
274       // project.  Maybe there was no project by that name?
275       String text = null;
276       try {
277         text = summaryPage.getText();
278       } catch (final IOException kaboom) {
279         throw new SourceForgeException(kaboom);
280       }
281       assertNotNull(text, "text");
282       if (text.indexOf("Invalid Project") >= 0) {
283         throw new NoSuchProjectException(projectShortName);
284       }
285       // Treat any other kind of error as a UI change.
286       throw new SourceForgeUIChangeException();
287     }
288 
289     // Examine the link's destination URL to see what the project ID is.
290     // Sometimes SourceForge calls projects projects, and sometimes it calls
291     // them groups.  In this case we're looking for the value of the "group_id"
292     // parameter.
293     final String linkText = viewMembersLink.getURLString();
294     assertNotNull(linkText, "linkText");
295     final int groupIDIndex = linkText.indexOf("?group_id=");
296     if (groupIDIndex < 0) {
297       throw new SourceForgeUIChangeException();
298     }
299     int projectID = -1;
300     final String projectIDString = 
301       linkText.substring(groupIDIndex + "?group_id=".length());
302 
303     // If we found a group/project ID, convert it to an integer, stuff it in the
304     // cache, and return it.
305     if (projectIDString != null) {
306       try {
307         projectID = Integer.parseInt(projectIDString);
308       } catch (final NumberFormatException kaboom) {
309         projectID = -1;
310       }
311     }
312     PROJECT_ID_MAP.put(projectShortName, new Integer(projectID));
313     return projectID;
314   }
315 
316   /***
317    * Extracts a {@link WebForm} from the supplied array of {@link WebForm}s,
318    * provided that its {@linkplain WebForm#getAction() action} is equal to the
319    * supplied {@link String}.  This method may return <code>null</code>.
320    *
321    * @param      forms
322    *               the array of {@link WebForm}s to consider; if
323    *               <code>null</code> then <code>null</code> will be returned
324    * @param      action
325    *               the action {@link String} to which a given {@link WebForm}'s
326    *               {@linkplain WebForm#getAction() action} must be equal in
327    *               order for that {@link WebForm} to be returned; if
328    *               <code>null</code> then <code>null</code> will be returned
329    * @return     a {@link WebForm} from the supplied array of {@link WebForm}s,
330    *               provided that its {@linkplain WebForm#getAction() action} is
331    *               equal to the supplied {@link String}, or <code>null</code>
332    */
333   protected WebForm findFormWithAction(final WebForm[] forms,
334                                        final String action) {
335     if (forms == null || forms.length <= 0) {
336       return null;
337     }
338     WebForm form;
339     String formAction;
340     for (int i = 0; i < forms.length; i++) {
341       form = forms[i];
342       if (form != null) {
343         formAction = form.getAction();
344         if (formAction == null) {
345           if (action == null) {
346             return null;
347           }
348         } else {
349           if (formAction.equals(action)) {
350             return form;
351           }
352         }
353       }
354     }
355     return null;
356   }
357 
358   /***
359    * Returns an array of {@link WebForm}s whose elements are those {@link
360    * WebForm}s extracted from the supplied {@link WebForm} array with
361    * {@linkplain WebForm#getAction() actions} equal to the supplied {@link
362    * String}.  This method never returns <code>null</code>.
363    *
364    * @param      forms
365    *               the {@link WebForm} array in which to find candidates; may 
366    *               be <code>null</code>
367    * @param      action
368    *               the {@linkplain WebForm#getAction() action} {@link String} to
369    *               match; may be <code>null</code>
370    * @return     an array of {@link WebForm}s whose elements are those {@link
371    *               WebForm}s extracted from the supplied {@link WebForm} array
372    *               with {@linkplain WebForm#getAction() actions} equal to the
373    *               supplied {@link String}; never <code>null</code>
374    */
375   protected WebForm[] retainFormsWithAction(final WebForm[] forms,
376                                             final String action) {
377     if (forms == null || forms.length <= 0) {
378       return new WebForm[0];
379     }
380     final Collection returnForms = new ArrayList(forms.length);
381     WebForm form;
382     String formAction;
383     for (int i = 0; i < forms.length; i++) {
384       form = forms[i];
385       if (form != null) {
386         formAction = form.getAction();
387         if (formAction == null) {
388           if (action == null) {
389             returnForms.add(form);
390           }
391         } else {
392           if (formAction.equals(action)) {
393             returnForms.add(form);
394           }
395         }
396       }
397     }
398     return (WebForm[])returnForms.toArray(new WebForm[returnForms.size()]);
399   }
400 
401   protected WebForm[] retainFormsWithParameterNamed(final WebForm[] forms,
402                                                     final String parameter) {
403     if (forms == null || forms.length <= 0) {
404       return new WebForm[0];
405     }
406     final Collection returnForms = new ArrayList(forms.length);
407     WebForm form;
408     for (int i = 0; i < forms.length; i++) {
409       form = forms[i];
410       if (form != null && form.hasParameterNamed(parameter)) {
411         returnForms.add(form);
412       }
413     }
414     return (WebForm[])returnForms.toArray(new WebForm[returnForms.size()]);
415   }
416 
417   protected WebForm[] retainFormsWithParameterValue(final WebForm[] forms,
418                                                     final String parameter,
419                                                     final String value) {
420     if (forms == null || forms.length <= 0) {
421       return new WebForm[0];
422     }
423     final Collection returnForms = new ArrayList(forms.length);
424     WebForm form;
425     String parameterValue;
426     for (int i = 0; i < forms.length; i++) {
427       form = forms[i];
428       if (form != null) {
429         parameterValue = form.getParameterValue(parameter);
430         if (parameterValue != null && parameterValue.equals(value)) {
431           returnForms.add(form);
432         }
433       }
434     }
435     return (WebForm[])returnForms.toArray(new WebForm[returnForms.size()]);
436   }
437 
438   /***
439    * A convenience method that throws a {@link SourceForgeException} if the
440    * supplied {@link Object} is <code>null</code>.  This method is primarily
441    * used to check method arguments and in other cases where a checked {@link
442    * Exception} is preferred to the usual {@link IllegalArgumentException}
443    * alternative.
444    *
445    * @param      object
446    *               the {@link Object} to check; if <code>null</code> a {@link
447    *               SourceForgeException} will be thrown
448    * @param      message
449    *               a message describing the problem if the supplied {@link
450    *               Object} is <code>null</code>; may be <code>null</code>
451    * @exception  SourceForgeException
452    *               if the supplied {@link Object} is <code>null</code>
453    */
454   protected static final void assertNotNull(final Object object,
455                                             final String message)
456     throws SourceForgeException {
457     if (object == null) {
458       throw new SourceForgeException(message);
459     }
460   }
461 
462   /***
463    * A convenience metohd that throws either a {@link SourceForgeException} or an
464    * {@link SourceForgeException} if the supplied {@link Object} array is
465    * <code>null</code> or has a length less than or equal to <code>0</code>
466    * respectively.
467    *
468    * @param      object
469    *               the {@link Object} array to check; if <code>null</code> or
470    *               empty a {@link SourceForgeException} will be thrown
471    * @param      message
472    *               a message describing the problem if the supplied {@link
473    *               Object} array is <code>null</code> or empty; may be
474    *               <code>null</code>
475    * @exception  SourceForgeException
476    *               if the supplied {@link Object} array is <code>null</code> or
477    *               empty
478    */
479   protected static final void assertArrayFull(final Object[] object,
480                                               final String message)
481     throws SourceForgeException, SourceForgeException {
482     assertNotNull(object, message);
483     if (object.length <= 0) {
484       throw new SourceForgeException(message);
485     }
486   }
487 
488 }