View Javadoc

1   /*
2    * Copyright (c) 2004 UNINETT FAS
3    *
4    * This program is free software; you can redistribute it and/or modify it
5    * under the terms of the GNU General Public License as published by the Free
6    * Software Foundation; either version 2 of the License, or (at your option)
7    * any later version.
8    *
9    * This program is distributed in the hope that it will be useful, but WITHOUT
10   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11   * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12   * more details.
13   *
14   * You should have received a copy of the GNU General Public License along with
15   * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
16   * Place - Suite 330, Boston, MA 02111-1307, USA.
17   *
18   * $Id: RequestUtil.java,v 1.56 2006/01/16 16:06:43 indal Exp $
19   */
20  
21  package no.feide.moria.servlet;
22  
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.Enumeration;
26  import java.util.HashMap;
27  import java.util.Locale;
28  import java.util.MissingResourceException;
29  import java.util.Properties;
30  import java.util.ResourceBundle;
31  import java.util.StringTokenizer;
32  import java.util.TreeMap;
33  import java.util.Vector;
34  
35  import javax.servlet.ServletContext;
36  import javax.servlet.http.Cookie;
37  
38  import no.feide.moria.log.MessageLogger;
39  
40  /***
41   * This class is a toolkit for the servlets. Its main functionality is to
42   * retrieve resource bundles.
43   * @author Lars Preben S. Arnesen <lars.preben.arnesen@conduct.no>
44   * @version $Revision: 1.56 $
45   */
46  public final class RequestUtil {
47  
48      /***
49       * Used for logging.
50       */
51      static MessageLogger log = new MessageLogger(RequestUtil.class);
52  
53      /***
54       * Prefix for all web module properties. <br>
55       * <br>
56       * Current value is <code>"no.feide.moria.web."</code>.
57       */
58      private static final String PATH_PREFIX = "no.feide.moria.web.";
59  
60      /***
61       * Property name for web module configuration. <br>
62       * <br>
63       * Current value is <code>PATH_PREFIX + "config"</code>.
64       */
65      public static final String PROP_CONFIG = PATH_PREFIX + "config";
66  
67      /***
68       * Configuration property giving the name of the URL parameter containing
69       * the Moria ticket ID. <em>This property is required.</em><br>
70       * <br>
71       * Current value is <code>PATH_PREFIX + "login.ticket_param"</code>.
72       */
73      public static final String PROP_LOGIN_TICKET_PARAM = PATH_PREFIX + "login.ticket_param";
74  
75      /***
76       * Property name for Organization.
77       */
78      public static final String PROP_ORG = PATH_PREFIX + "org";
79  
80      /***
81       * Configuration property giving the default Moria language, to be used if
82       * no other preferences (user or service) are available.
83       * <em>This property is required.</em><br>
84       * <br>
85       * Current value is <code>PATH_PREFIX + "login.default_language"</code>.
86       */
87  
88      public static final String PROP_LOGIN_DEFAULT_LANGUAGE = PATH_PREFIX + "login.default_language";
89  
90      /***
91       * Property name for Language.
92       */
93      public static final String PROP_LANGUAGE = PATH_PREFIX + "lang";
94  
95      /***
96       * Configuration property giving the URL of the login servlet.
97       * <em>This property is required.</em><br>
98       * <br>
99       * Current value is <code>PATH_PREFIX + "login.url_prefix"</code>.
100      */
101     public static final String PROP_LOGIN_URL_PREFIX = PATH_PREFIX + "login.url_prefix";
102 
103     /***
104      * Configuration property giving the URL of the information servlet.
105      * <em>This property is required.</em><br>
106      * <br>
107      * Current value is <code>PATH_PREFIX + "information.url_prefix"</code>.
108      */
109     public static final String PROP_INFORMATION_URL_PREFIX = PATH_PREFIX + "information.url_prefix";
110 
111     /***
112      * Configuration property sub-string giving the abbreviations and names for
113      * all available languages. The property name is on the form
114      * <code>PROP_LANGUAGE + "_" + PROP_COMMON</code>.<br>
115      * <br>
116      * The actual values are a comma separated list of elements on the form
117      * <code>EN:English</code>, that is, a two-letter language abbreviation
118      * and a ':' character followed by its display name. <br>
119      * <br>
120      * <em>This property is required</em><br>
121      * <br>
122      * Current value is <code>"common"</code>.
123      */
124     public static final String PROP_COMMON = "common";
125 
126     /***
127      * Configuration property giving the name of the cookie used to remember the
128      * user's previously selected organization.
129      * <em>This property is required.</em><br>
130      * <br>
131      * Current value is <code>PATH_PREFIX + "cookie.org.name"</code>.
132      */
133     public static final String PROP_COOKIE_ORG = PATH_PREFIX + "cookie.org.name";
134 
135     /***
136      * Configuration property giving the lifetime, in hours, for the cookie used
137      * to remember the user's previously selected organization.
138      * <em>This property is required.</em> <br>
139      * <br>
140      * Current value is <code>PATH_PREFIX + "cookie.org.ttl"</code>.
141      * @see #PROP_COOKIE_ORG
142      */
143     public static final String PROP_COOKIE_ORG_TTL = PATH_PREFIX + "cookie.org.ttl";
144 
145     /***
146      * Configuration property giving the name of the cookie used to set user's
147      * desired language. <em>This property is required.</em><br>
148      * <br>
149      * Current value is <code>PATH_PREFIX + "cookie.lang.name"</code>.
150      */
151     public static final String PROP_COOKIE_LANG = PATH_PREFIX + "cookie.lang.name";
152 
153     /***
154      * Configuration property giving the lifetime, in hours, for the cookie used
155      * to set user's desired language. <em>This property is required.</em>
156      * <br>
157      * <br>
158      * Current value is <code>PATH_PREFIX + "cookie.lang.ttl"</code>.
159      */
160     public static final String PROP_COOKIE_LANG_TTL = PATH_PREFIX + "cookie.lang.ttl";
161 
162     /***
163      * Configuration property giving the name of the cookie used to carry SSO
164      * sessions (that is, a Moria ticket ID belonging to a SSO session).
165      * <em>This property is required.</em><br>
166      * <br>
167      * Current value is <code>PATH_PREFIX + "cookie.sso.name"</code>.
168      */
169     public static final String PROP_COOKIE_SSO = PATH_PREFIX + "cookie.sso.name";
170 
171     /***
172      * Configuration property giving the lifetime, in hours, for the cookie used
173      * to carry SSO sessions (named by <code>PROP_COOKIE_SSO</code>).
174      * <em>This property is required.</em><br>
175      * <br>
176      * Current value is <code>PATH_PREFIX + "cookie.sso.ttl"</code>.
177      */
178     public static final String PROP_COOKIE_SSO_TTL = PATH_PREFIX + "cookie.sso.ttl";
179 
180     /***
181      * Configuration property giving the name of the cookie used to deny SSO
182      * (when using Moria on public computers, for example).
183      * <em>This property is required.</em>.<br>
184      * <br>
185      * Current value is <code>PATH_PREFIX + "cookie.denysso.name"</code>.
186      */
187     public static final String PROP_COOKIE_DENYSSO = PATH_PREFIX + "cookie.denysso.name";
188 
189     /***
190      * Configuration property giving the lifetime, in hours, for the cookie used
191      * to deny SSO (named by <code>PROP_COOKIE_DENYSSO</code>).
192      * <em>This property is required</em><br>
193      * <br>
194      * Current value is <code>PATH_PREFIX + "cookie.denysso.ttl"</code>.
195      * @see #PROP_COOKIE_DENYSSO
196      */
197     public static final String PROP_COOKIE_DENYSSO_TTL = PATH_PREFIX + "cookie.denysso.ttl";
198 
199     /***
200      * Property name for Logout URL.
201      */
202     public static final String PROP_LOGOUT_URL_PARAM = PATH_PREFIX + "logout.url_param";
203 
204     /***
205      * The name of the resource bundle for the login page. <br>
206      * <br>
207      * Current value is <code>"login"</code>.
208      */
209     public static final String BUNDLE_LOGIN = "login";
210 
211     /***
212      * Bundle for the Welcome page for the Information Service.
213      */
214     public static final String BUNDLE_INFOWELCOME = "infowelcome";
215 
216     /***
217      * Bundle for the Statistics page.
218      */
219     public static final String BUNDLE_STATISTICSSERVLET = "statistics";
220 
221     /***
222      * Configuration property for the Information service's attribute
223      * description XML file.
224      */
225     public static final String PROP_INFORMATION_DESCRIPTIONS = PATH_PREFIX + "information.descriptions";
226 
227     /***
228      * Configuration property for the StatusServlet status.xml file path.
229      */
230     public static final String PROP_BACKENDSTATUS_STATUS_XML = PATH_PREFIX + "backendstatus.status_xml";
231 
232     /***
233      * Configuration property for the StatusServlet statistics.xml file path.
234      */
235     public static final String PROP_BACKENDSTATUS_STATISTICS_XML = PATH_PREFIX + "backendstatus.statistics_xml";
236 
237     /***
238      * Configuration property for the StatusServlet statistics2 basename file path.
239      */
240     public static final String PROP_BACKENDSTATUS_STATISTICS2_BASENAME_XML = PATH_PREFIX + "backendstatus.statistics2_basename_xml";
241 
242     /***
243      * Configuration property for the StatusServlet statistics2.xml file path.
244      */
245     public static final String PROP_BACKENDSTATUS_STATISTICS2_XML = PATH_PREFIX + "backendstatus.statistics2_xml";
246 
247     /***
248      * Configuration property for the StatisticsServlet's ignored services
249      */
250     public static final String PROP_BACKENDSTATUS_IGNORE = PATH_PREFIX + "backendstatus.ignore";
251 
252     /***
253      * Bundle for the Information servlet's attribute descriptions. 
254      */
255     public static final String BUNDLE_INFORMATIONSERVLET = "attributes";
256 
257     /***
258      * Bundle for the information page about the information servlet.
259      */
260     public static final String BUNDLE_INFOABOUT = "infoabout";
261 
262     /***
263      * Bundle for the status servlet.
264      */
265     public static final String BUNDLE_STATUSSERVLET = "status";
266 
267     /***
268      * Bundle for the faq page.
269      */
270     public static final String BUNDLE_FAQ = "faq";
271 
272     /***
273      * Bundle for the error page.
274      */
275     public static final String BUNDLE_ERROR = "error";
276 
277     /***
278      * Link to faq, shown on the login page.
279      */
280     public static final String FAQ_LINK = PATH_PREFIX + "faqlink";
281 
282     /***
283      * URL to the Status servlet, as shown on the FAQ page.<br>
284      * <em>This property is required.</em> <br>
285      * Current value is <code>PATH_PREFIX + "faq.status"</code>.
286      */
287     public static final String PROP_FAQ_STATUS = PATH_PREFIX + "faq.status";
288 
289     /***
290      * URL to the Statistics servlet, used for language selection.
291      */
292     public static final String PROP_STATISTICS_URL = PATH_PREFIX + "statistics.url";
293 
294     /***
295      * Organization name of the Moria owner, as shown on the FAQ page.<br>
296      * <br>
297      * Current value is <code>PATH_PREFIX + "faq.owner"</code>.
298      */
299     public static final String PROP_FAQ_OWNER = PATH_PREFIX + "faq.owner";
300 
301     /***
302      * Link to pictures from the information service.
303      */
304     public static final String PIC_LINK = PATH_PREFIX + "piclink";
305 
306     /***
307      * TODO: Add JavaDoc. Is this in use?
308      */
309     public static final String RESOURCE_MAIL = PATH_PREFIX + "resource.mail";
310 
311     /***
312      * TODO: Add JavaDoc. Is this in use?
313      */
314     public static final String RESOURCE_DATE = PATH_PREFIX + "resource.date";
315 
316     /***
317      * TODO: Add JavaDoc. Is this in use?
318      */
319     public static final String RESOURCE_LINK = PATH_PREFIX + "resource.link";
320 
321     /***
322      * Name of property from authorization module giving the default language
323      * for a given service. <em>This property is required.</em><br>
324      * <br>
325      * Current value is <code>"language"</code>.
326      */
327     // TODO: This constant should also be used in AuthenticationManager, instead
328     // of hard-coded "language" string.
329     public static final String CONFIG_LANG = "language";
330 
331     /***
332      * From Authorization config Home organization of service.
333      */
334     public static final String CONFIG_HOME = "home";
335 
336     /***
337      * From Authorization config: Service name.
338      */
339     public static final String CONFIG_DISPLAY_NAME = "displayName";
340 
341     /***
342      * From Authorization configuration; service principal.
343      */
344     public static final String CONFIG_SERVICE_PRINCIPAL = "name";
345 
346     /***
347      * From Authorization config: Service URL.
348      */
349     public static final String CONFIG_URL = "url";
350 
351     /***
352      * Parameter in request object: Username.
353      */
354     public static final String PARAM_USERNAME = "username";
355 
356     /***
357      * Parameter in request object: Password.
358      */
359     public static final String PARAM_PASSWORD = "password";
360 
361     /***
362      * Organization URL parameter, used when overriding default organization in
363      * the URL redirecting a user from a service to Moria. Useful for services
364      * that wish to dynamically force use of a certain organization irrespective
365      * of the user's previous selections or the service defaults. <br>
366      * <br>
367      * Current value is <code>"org"</code>.
368      */
369     public static final String PARAM_ORG = "org";
370 
371     /***
372      * Language URL parameter, used when overriding current language settings.
373      * <br>
374      * <br>
375      * Current value is <code>"language"</code>.
376      */
377     public static final String PARAM_LANG = "language";
378     
379     /***
380      * Attribute URL parameter, used when attributes are shown on the login page.
381      */
382     public static final String PARAM_SHOW_ATTRS = "showAttributes";
383 
384     /***
385      * Attribute URL parameter, used to show statistics from the previous year.
386      */
387     public static final String PARAM_OLD_STATISTICS = "old_statistics";
388     /***
389      * Parameter in request object: Deny SSO.
390      */
391     public static final String PARAM_DENYSSO = "denySSO";
392 
393     /***
394      * Base URL attribute in request object. Used to fill in the URL to the
395      * authentication web page. <br>
396      * <br>
397      * Current value is <code>"baseURL"</code>.
398      */
399     public static final String ATTR_BASE_URL = "baseURL";
400 
401     /***
402      * Attribute in request object: Security level.
403      */
404     // TODO: What does this actually mean?
405     public static final String ATTR_SEC_LEVEL = "secLevel";
406 
407     /***
408      * Attribute in request object: Error type.
409      */
410     public static final String ATTR_ERROR_TYPE = "errorType";
411 
412     /***
413      * Attribute in request object: Available languages.
414      */
415     public static final String ATTR_LANGUAGES = "languages";
416 
417     /***
418      * Attribute in request object: Available organizations.
419      */
420     public static final String ATTR_ORGANIZATIONS = "organizations";
421 
422     /***
423      * Attribute in request object: Preselected organization.
424      */
425     public static final String ATTR_SELECTED_ORG = "selectedOrg";
426 
427     /***
428      * Attribute in request object: Denial of SSO.
429      */
430     public static final String ATTR_SELECTED_DENYSSO = "selectedDenySSO";
431 
432     /***
433      * Attribute in request object: Preselected lanugage.
434      */
435     public static final String ATTR_SELECTED_LANG = "selectedLang";
436 
437     /***
438      * Attribute in request object: Name of client/service.
439      */
440     public static final String ATTR_CLIENT_NAME = "clientName";
441 
442     /***
443      * Attribute in request object: Link to associate with service name.
444      */
445     public static final String ATTR_CLIENT_URL = "clientURL";
446 
447     /***
448      * Attribute in request object: Language bundle.
449      */
450     public static final String ATTR_BUNDLE = "bundle";
451 
452     /***
453      * Attribute in request object: Requested attributes.
454      */
455     public static final String ATTR_REQUESTED_ATTRIBUTES = "requestedAttributes";
456 
457     /***
458      * Error type: No organization selected.
459      */
460     public static final String ERROR_NO_ORG = "noOrg";
461 
462     /***
463      * Error type: Invalid organization selected.
464      */
465     public static final String ERROR_INVALID_ORG = "invalidOrg";
466 
467     /***
468      * Error type: Authentication failed.
469      */
470     public static final String ERROR_AUTHENTICATION_FAILED = "authnFailed";
471 
472     /***
473      * Error type: Authorization failed.
474      */
475     public static final String ERROR_AUTHORIZATION_FAILED = "authorizationFailed";
476 
477     /***
478      * Error type: Unknown ticket.
479      */
480     public static final String ERROR_UNKNOWN_TICKET = "unknownTicket";
481 
482     /***
483      * Error type: The directory is down.
484      */
485     public static final String ERROR_DIRECTORY_DOWN = "directoryDown";
486 
487     /***
488      * Error type: Moria is unavailable.
489      */
490     public static final String ERROR_MORIA_DOWN = "moriaDown";
491 
492     /***
493      * Error type: User must supply username and password.
494      */
495     public static final String ERROR_NO_CREDENTIALS = "noCredentials";
496 
497 
498     /***
499      * Default private constructor.
500      */
501     private RequestUtil() {
502 
503     }
504 
505 
506     /***
507      * Generates a resource bundle. The language of the resource bundle is
508      * selected from the following priority list:
509      * <ol>
510      * <li>URL parameter (<code>requestParamLang</code>)
511      * <li>User's cookie (<code>langFromCookie</code>)
512      * <li>Default service language (<code>serviceLang</code>)
513      * <li>User's browser settings (<code>browserLang</code>)
514      * <li>Moria default setting (<code>moriaLang</code>)
515      * </ol>
516      * @param bundleName
517      *            Name of the resource bundle to retrieve. Cannot be
518      *            <code>null</code>.
519      * @param requestParamLang
520      *            Language specified from URL parameter. Can be
521      *            <code>null</code>.
522      * @param langFromCookie
523      *            Language specified from user's cookie. Can be
524      *            <code>null</code>.
525      * @param serviceLang
526      *            Default language as specified by configuration for the
527      *            service. Can be <code>null</code>.
528      * @param browserLang
529      *            Language as requested by the users browser. Can be
530      *            <code>null</code>.
531      * @param moriaLang
532      *            Default language for Moria. Cannot be <code>null</code>.
533      * @return The requested resource bundle.
534      * @throws IllegalArgumentException
535      *             If <code>bundleName</code> or <code>moriaLang</code> is
536      *             <code>null</code> or an empty string.
537      * @throws MissingResourceException
538      *             If the resource bundle cannot be found.
539      */
540     public static ResourceBundle getBundle(final String bundleName, final String requestParamLang, final String langFromCookie, final String serviceLang, final String browserLang, final String moriaLang) {
541 
542         // Sanity checks.
543         if (bundleName == null || bundleName.equals(""))
544             throw new IllegalArgumentException("Resource bundle name must be a non-empty string.");
545         if (moriaLang == null || moriaLang.equals(""))
546             throw new IllegalArgumentException("Default language must be a non-empty string.");
547 
548         // Build array of preferred language selections.
549         final Vector langSelections = new Vector();
550 
551         // Check URL parameter.
552         if (requestParamLang != null && !requestParamLang.equals(""))
553             langSelections.add(requestParamLang);
554 
555         // Check user cookie.
556         if (langFromCookie != null)
557             langSelections.add(langFromCookie);
558 
559         // Check service default.
560         if (serviceLang != null && !serviceLang.equals(""))
561             langSelections.add(serviceLang);
562 
563         // Check user's browser settings.
564         if (browserLang != null && !browserLang.equals("")) {
565             final String[] browserLangs = sortedAcceptLang(browserLang);
566             for (int i = 0; i < browserLangs.length; i++) {
567                 langSelections.add(browserLangs[i]);
568             }
569         }
570 
571         // Finally, add Moria default language.
572         langSelections.add(moriaLang);
573 
574         // Locate and return resulting resource bundle.
575         ResourceBundle bundle;
576         for (Enumeration e = langSelections.elements(); e.hasMoreElements();) {
577             bundle = locateBundle(bundleName, (String) e.nextElement());
578             if (bundle != null)
579                 return bundle;
580         }
581 
582         // No bundle found?
583         throw new MissingResourceException("Resource bundle not found", "ResourceBundle", bundleName);
584     }
585 
586 
587     /***
588      * Locates a bundle in a given language.
589      * @param bundleName
590      *            Name of the bundle, cannot be null or "".
591      * @param lang
592      *            The bundle's langauge.
593      * @return The resourceBundle for the selected language, null if it is not
594      *         found.
595      * @throws IllegalArgumentException
596      *             If <code>bundleName</code> or <code>lang</code> is
597      *             <code>null</code> or an empty string.
598      */
599     private static ResourceBundle locateBundle(final String bundleName, final String lang) {
600 
601         /* Validate parameters. */
602         if (bundleName == null || bundleName.equals("")) { throw new IllegalArgumentException("bundleName must be a non-empty string."); }
603         if (lang == null || lang.equals("")) { throw new IllegalArgumentException("lang must be a non-empty string."); }
604 
605         /* Find fallback resource bundle. */
606         ResourceBundle fallback;
607         try {
608             fallback = ResourceBundle.getBundle(bundleName, new Locale("bogus"));
609         } catch (MissingResourceException e) {
610             fallback = null;
611         }
612 
613         final Locale locale = new Locale(lang);
614         ResourceBundle bundle = null;
615         try {
616             bundle = ResourceBundle.getBundle(bundleName, locale);
617         } catch (MissingResourceException e) {
618             /* No bundle was found, ignore and move on. */
619         }
620 
621         if (bundle != fallback) { return bundle; }
622 
623         /* Check if the fallback is actually requested. */
624         if (bundle != null && bundle == fallback && locale.getLanguage().equals(Locale.getDefault().getLanguage())) { return bundle; }
625 
626         /* No bundle found. */
627         return null;
628     }
629 
630 
631     /***
632      * Returns a requested cookie value from the HTTP request.
633      * @param cookieName
634      *            Name of the cookie.
635      * @param cookies
636      *            The cookies from the HTTP request.
637      * @return Requested value, empty string if not found.
638      * @throws IllegalArgumentException
639      *             If <code>cookieName</code> is null or an empty string.
640      */
641     public static String getCookieValue(final String cookieName, final Cookie[] cookies) {
642 
643         // Sanity checks.
644         if (cookieName == null || cookieName.equals(""))
645             throw new IllegalArgumentException("Cookie name must be a non-empty string");
646         if (cookies == null)
647             return null;
648 
649         // Return cookie value, if set.
650         String value = null;
651         for (int i = 0; i < cookies.length; i++) {
652             if (cookies[i].getName().equals(cookieName))
653                 value = cookies[i].getValue();
654         }
655         return value;
656 
657     }
658 
659 
660     /***
661      * Creates a cookie.
662      * @param cookieName
663      *            Name of the cookie. Cannot be <code>null</code>.
664      * @param cookieValue
665      *            Value to set in cookie. Cannot be <code>null</code>.
666      * @param validHours
667      *            Number of hours before the cookie expires. Must be 0 or
668      *            greater.
669      * @return A Cookie with the specified name and value, with the given expiry
670      *         time.
671      * @throws IllegalArgumentException
672      *             If <code>cookieName</code> or <code>cookieValue</code> is
673      *             <code>null</code> or an empty string.
674      */
675     public static Cookie createCookie(final String cookieName, final String cookieValue, final int validHours) {
676 
677         // Sanity checks.
678         if (cookieName == null || cookieName.equals(""))
679             throw new IllegalArgumentException("Cookie name must be a non-empty string");
680         if (cookieValue == null || cookieValue.equals(""))
681             throw new IllegalArgumentException("Cookie value must be a non-empty string");
682 
683         // Create and return cookie.
684         final Cookie cookie = new Cookie(cookieName, cookieValue);
685         cookie.setMaxAge(validHours * 60 * 60); // Hours to seconds
686         cookie.setVersion(0);
687         return cookie;
688     }
689 
690 
691     /***
692      * Parses an Accept-Language header sent from a browser. The language
693      * entries in the string can be weighted and the parser generates a list of
694      * the languages sorted by the weight value.
695      * @param acceptLang
696      *            The accept language header, cannot be null or "".
697      * @return A string array of language names, sorted by the browser's weight
698      *         preferences.
699      * @throws IllegalArgumentException
700      *             If <code>acceptLang</code> is null or an empty string.
701      */
702     static String[] sortedAcceptLang(final String acceptLang) {
703 
704         if (acceptLang == null || acceptLang.equals("")) { throw new IllegalArgumentException("acceptLang must be a non-empty string."); }
705 
706         final StringTokenizer tokenizer = new StringTokenizer(acceptLang, ",");
707         final HashMap weightedLangs = new HashMap();
708 
709         while (tokenizer.hasMoreTokens()) {
710             final String token = tokenizer.nextToken();
711             String lang = token;
712             boolean ignore = false;
713             String weight = "1.0";
714             int index;
715 
716             /* Language and weighting are devided by ";". */
717             if ((index = token.indexOf(";")) != -1) {
718                 String parsedWeight;
719                 lang = token.substring(0, index);
720 
721                 /* Weight data. */
722                 parsedWeight = token.substring(index + 1, token.length());
723                 parsedWeight = parsedWeight.trim();
724                 if (parsedWeight.startsWith("q=")) {
725                     parsedWeight = parsedWeight.substring(2, parsedWeight.length());
726                     weight = parsedWeight;
727                 } else {
728                     /* Format error, flag to ignore token. */
729                     ignore = true;
730                 }
731             }
732 
733             if (!ignore) {
734                 lang = lang.trim();
735 
736                 /* Country and language is devided by "-" (optional). */
737                 if ((index = lang.indexOf("-")) != -1) {
738                     lang = lang.substring(index + 1, lang.length());
739                 }
740 
741                 weightedLangs.put(weight, lang);
742             }
743         }
744 
745         final Vector sortedLangs = new Vector();
746         final String[] sortedKeys = (String[]) weightedLangs.keySet().toArray(new String[weightedLangs.size()]);
747         Arrays.sort(sortedKeys, Collections.reverseOrder());
748 
749         for (int i = 0; i < sortedKeys.length; i++) {
750             sortedLangs.add(weightedLangs.get(sortedKeys[i]));
751         }
752 
753         return (String[]) sortedLangs.toArray(new String[sortedLangs.size()]);
754     }
755 
756 
757     /***
758      * Reads institution names from the servlet configuration and generates a
759      * TreeMap with the result, using the correct language. <br>
760      * <br>
761      * The configuration <em>must</em> contain properties on the form
762      * <code>element + "_" + language</code>.
763      * @param config
764      *            The web module configuration.
765      * @param element
766      *            The sub-element of the web module configuration to process.
767      * @param language
768      *            The language used when generating institution names.
769      * @return A <code>TreeMap</code> of institution names with full name as
770      *         key and ID as value.
771      * @throws IllegalArgumentException
772      *             If <code>config</code> is <code>null</code>, or if
773      *             <code>element</code> or <code>language</code> is
774      *             <code>null</code> or an empty string.
775      * @throws IllegalStateException
776      *             If no elements of type <code>element</code> are found in
777      *             the configuration <code>config</code>. Also thrown if the
778      *             values found in <code>config</code> contains less than or
779      *             more than one occurrence of the ':' separator character.
780      */
781     static TreeMap parseConfig(final Properties config, final String element, final String language) {
782 
783         // Sanity checks.
784         if (config == null)
785             throw new IllegalArgumentException("Configuration cannot be null");
786         if (element == null || element.equals(""))
787             throw new IllegalArgumentException("Configuration element must be a non-empty string");
788         if (language == null || language.equals(""))
789             throw new IllegalArgumentException("Institution name language must be a non-empty string");
790 
791         // Get the property value from configuration, with sanity check.
792         final String value = config.getProperty(element + "_" + language);
793         if (value == null)
794             throw new IllegalStateException("No elements of type '" + element + "' in configuration.");
795 
796         // Process each ","-separated language declaration.
797         final StringTokenizer tokenizer = new StringTokenizer(value, ",");
798         final TreeMap names = new TreeMap();
799         while (tokenizer.hasMoreTokens()) {
800             final String token = tokenizer.nextToken();
801 
802             // Do we have the correct separator in the language declaration?
803             final int index = token.indexOf(":");
804             if (index == -1)
805                 throw new IllegalStateException("Missing ':' separator in language declaration: '" + token + "'");
806 
807             // Separate language declaration into two-letter abbreviation and
808             // display name.
809             final String shortName = token.substring(0, index);
810             final String longName = token.substring(index + 1, token.length());
811 
812             // Do we have more than one separator in this language declaration?
813             if (shortName.indexOf(":") != -1 || longName.indexOf(":") != -1)
814                 throw new IllegalStateException("Config has wrong format.");
815 
816             // Add all known organizations (from Web Module configuration) to
817             // list.
818             names.put(longName, shortName);
819         }
820 
821         // Return the list of names.
822         return names;
823     }
824 
825 
826     /***
827      * Replaces a given token with hyperlinks. The URL and name of the hyperlink
828      * is given as parameters. Every occurance of the token in the data string
829      * is replaced by a hyperlink.
830      * @param token
831      *            The token to replace with link.
832      * @param data
833      *            The data containing text and token(s).
834      * @param name
835      *            The link text.
836      * @param url
837      *            The URL to link to.
838      * @return A string with hyperlinks in stead of tokens.
839      * @throws IllegalArgumentException
840      *             If token, data, name or url is null or an empty string.
841      */
842     public static String insertLink(final String token, final String data, final String name, final String url) {
843 
844         /* Validate parameters */
845         if (token == null || token.equals("")) { throw new IllegalArgumentException("token must be a non-empty string"); }
846         if (data == null || data.equals("")) { throw new IllegalArgumentException("data must be a non-empty string"); }
847         if (name == null || name.equals("")) { throw new IllegalArgumentException("name must be a non-empty string"); }
848         if (url == null || url.equals("")) { throw new IllegalArgumentException("url must be a non-empty string"); }
849 
850         final String link = "<a href=\"" + url + "\">" + name + "</a>";
851 
852         return data.replaceAll(token, link);
853     }
854 
855 
856     /***
857      * Get the config from the context. The configuration is expected to be set
858      * by the controller before requests are sent to this servlet.
859      * @param context
860      *            ServletContext containing the configuration.
861      * @return The configuration.
862      * @throws IllegalStateException
863      *             If config is not properly set in the context.
864      */
865     static Properties getConfig(final ServletContext context) {
866 
867         /* Validate parameters */
868         if (context == null) { throw new IllegalArgumentException("context cannot be null."); }
869 
870         final Properties config;
871 
872         /* Validate config */
873         try {
874             config = (Properties) context.getAttribute("no.feide.moria.web.config");
875         } catch (ClassCastException e) {
876             throw new IllegalStateException("Config is not correctly set in context. Not a java.util.Properties object.");
877         }
878 
879         if (config == null) { throw new IllegalStateException("Config is not set in context."); }
880 
881         return config;
882     }
883 }