View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package javax.mail;
21  
22  import java.io.BufferedReader;
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.InputStreamReader;
28  import java.io.PrintStream;
29  import java.lang.reflect.Constructor;
30  import java.lang.reflect.InvocationTargetException;
31  import java.net.InetAddress;
32  import java.net.URL;
33  import java.util.ArrayList;
34  import java.util.Enumeration;
35  import java.util.HashMap;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Properties;
39  import java.util.StringTokenizer;
40  import java.util.WeakHashMap;
41  
42  
43  /**
44   * OK, so we have a final class in the API with a heck of a lot of implementation required...
45   * let's try and figure out what it is meant to do.
46   * <p/>
47   * It is supposed to collect together properties and defaults so that they can be
48   * shared by multiple applications on a desktop; with process isolation and no
49   * real concept of shared memory, this seems challenging. These properties and
50   * defaults rely on system properties, making management in a app server harder,
51   * and on resources loaded from "mail.jar" which may lead to skew between                    
52   * differnet independent implementations of this API.
53   *
54   * @version $Rev: 702537 $ $Date: 2008-10-07 12:39:34 -0400 (Tue, 07 Oct 2008) $
55   */
56  public final class Session {
57      private static final Class[] PARAM_TYPES = {Session.class, URLName.class};
58      private static final WeakHashMap addressMapsByClassLoader = new WeakHashMap();
59      private static Session DEFAULT_SESSION;
60  
61      private Map passwordAuthentications = new HashMap();
62  
63      private final Properties properties;
64      private final Authenticator authenticator;
65      private boolean debug;
66      private PrintStream debugOut = System.out;
67  
68      private static final WeakHashMap providersByClassLoader = new WeakHashMap();
69  
70      /**
71       * No public constrcutor allowed.
72       */
73      private Session(Properties properties, Authenticator authenticator) {
74          this.properties = properties;
75          this.authenticator = authenticator;
76          debug = Boolean.valueOf(properties.getProperty("mail.debug")).booleanValue();
77      }
78  
79      /**
80       * Create a new session initialized with the supplied properties which uses the supplied authenticator.
81       * Clients should ensure the properties listed in Appendix A of the JavaMail specification are
82       * set as the defaults are unlikey to work in most scenarios; particular attention should be given
83       * to:
84       * <ul>
85       * <li>mail.store.protocol</li>
86       * <li>mail.transport.protocol</li>
87       * <li>mail.host</li>
88       * <li>mail.user</li>
89       * <li>mail.from</li>
90       * </ul>
91       *
92       * @param properties    the session properties
93       * @param authenticator an authenticator for callbacks to the user
94       * @return a new session
95       */
96      public static Session getInstance(Properties properties, Authenticator authenticator) {
97          return new Session(new Properties(properties), authenticator);
98      }
99  
100     /**
101      * Create a new session initialized with the supplied properties with no authenticator.
102      *
103      * @param properties the session properties
104      * @return a new session
105      * @see #getInstance(java.util.Properties, Authenticator)
106      */
107     public static Session getInstance(Properties properties) {
108         return getInstance(properties, null);
109     }
110 
111     /**
112      * Get the "default" instance assuming no authenticator is required.
113      *
114      * @param properties the session properties
115      * @return if "default" session
116      * @throws SecurityException if the does not have permission to access the default session
117      */
118     public synchronized static Session getDefaultInstance(Properties properties) {
119         return getDefaultInstance(properties, null);
120     }
121 
122     /**
123      * Get the "default" session.
124      * If there is not current "default", a new Session is created and installed as the default.
125      *
126      * @param properties
127      * @param authenticator
128      * @return if "default" session
129      * @throws SecurityException if the does not have permission to access the default session
130      */
131     public synchronized static Session getDefaultInstance(Properties properties, Authenticator authenticator) {
132         if (DEFAULT_SESSION == null) {
133             DEFAULT_SESSION = getInstance(properties, authenticator);
134         } else {
135             if (authenticator != DEFAULT_SESSION.authenticator) {
136                 if (authenticator == null || DEFAULT_SESSION.authenticator == null || authenticator.getClass().getClassLoader() != DEFAULT_SESSION.authenticator.getClass().getClassLoader()) {
137                     throw new SecurityException();
138                 }
139             }
140             // todo we should check with the SecurityManager here as well
141         }
142         return DEFAULT_SESSION;
143     }
144 
145     /**
146      * Enable debugging for this session.
147      * Debugging can also be enabled by setting the "mail.debug" property to true when
148      * the session is being created.
149      *
150      * @param debug the debug setting
151      */
152     public void setDebug(boolean debug) {
153         this.debug = debug;
154     }
155 
156     /**
157      * Get the debug setting for this session.
158      *
159      * @return the debug setting
160      */
161     public boolean getDebug() {
162         return debug;
163     }
164 
165     /**
166      * Set the output stream where debug information should be sent.
167      * If set to null, System.out will be used.
168      *
169      * @param out the stream to write debug information to
170      */
171     public void setDebugOut(PrintStream out) {
172         debugOut = out == null ? System.out : out;
173     }
174 
175     /**
176      * Return the debug output stream.
177      *
178      * @return the debug output stream
179      */
180     public PrintStream getDebugOut() {
181         return debugOut;
182     }
183 
184     /**
185      * Return the list of providers available to this application.
186      * This method searches for providers that are defined in the javamail.providers
187      * and javamail.default.providers resources available through the current context
188      * classloader, or if that is not available, the classloader that loaded this class.
189      * <p/>
190      * As searching for providers is potentially expensive, this implementation maintains
191      * a WeakHashMap of providers indexed by ClassLoader.
192      *
193      * @return an array of providers
194      */
195     public Provider[] getProviders() {
196         ProviderInfo info = getProviderInfo();
197         return (Provider[]) info.all.toArray(new Provider[info.all.size()]);
198     }
199 
200     /**
201      * Return the provider for a specific protocol.
202      * This implementation initially looks in the Session properties for an property with the name
203      * "mail.<protocol>.class"; if found it attempts to create an instance of the class named in that
204      * property throwing a NoSuchProviderException if the class cannot be loaded.
205      * If this property is not found, it searches the providers returned by {@link #getProviders()}
206      * for a entry for the specified protocol.
207      *
208      * @param protocol the protocol to get a provider for
209      * @return a provider for that protocol
210      * @throws NoSuchProviderException
211      */
212     public Provider getProvider(String protocol) throws NoSuchProviderException {
213         ProviderInfo info = getProviderInfo();
214         Provider provider = null;
215         String providerName = properties.getProperty("mail." + protocol + ".class");
216         if (providerName != null) {
217             provider = (Provider) info.byClassName.get(providerName);
218             if (debug) {
219                 writeDebug("DEBUG: new provider loaded: " + provider.toString());
220             }
221         }
222 
223         // if not able to locate this by class name, just grab a registered protocol.
224         if (provider == null) {
225             provider = (Provider) info.byProtocol.get(protocol);
226         }
227 
228         if (provider == null) {
229             throw new NoSuchProviderException("Unable to locate provider for protocol: " + protocol);
230         }
231         if (debug) {
232             writeDebug("DEBUG: getProvider() returning provider " + provider.toString());
233         }
234         return provider;
235     }
236 
237     /**
238      * Make the supplied Provider the default for its protocol.
239      *
240      * @param provider the new default Provider
241      * @throws NoSuchProviderException
242      */
243     public void setProvider(Provider provider) throws NoSuchProviderException {
244         ProviderInfo info = getProviderInfo();
245         info.byProtocol.put(provider.getProtocol(), provider);
246     }
247 
248     /**
249      * Return a Store for the default protocol defined by the mail.store.protocol property.
250      *
251      * @return the store for the default protocol
252      * @throws NoSuchProviderException
253      */
254     public Store getStore() throws NoSuchProviderException {
255         String protocol = properties.getProperty("mail.store.protocol");
256         if (protocol == null) {
257             throw new NoSuchProviderException("mail.store.protocol property is not set");
258         }
259         return getStore(protocol);
260     }
261 
262     /**
263      * Return a Store for the specified protocol.
264      *
265      * @param protocol the protocol to get a Store for
266      * @return a Store
267      * @throws NoSuchProviderException if no provider is defined for the specified protocol
268      */
269     public Store getStore(String protocol) throws NoSuchProviderException {
270         Provider provider = getProvider(protocol);
271         return getStore(provider);
272     }
273 
274     /**
275      * Return a Store for the protocol specified in the given URL
276      *
277      * @param url the URL of the Store
278      * @return a Store
279      * @throws NoSuchProviderException if no provider is defined for the specified protocol
280      */
281     public Store getStore(URLName url) throws NoSuchProviderException {
282         return (Store) getService(getProvider(url.getProtocol()), url);
283     }
284 
285     /**
286      * Return the Store specified by the given provider.
287      *
288      * @param provider the provider to create from
289      * @return a Store
290      * @throws NoSuchProviderException if there was a problem creating the Store
291      */
292     public Store getStore(Provider provider) throws NoSuchProviderException {
293         if (Provider.Type.STORE != provider.getType()) {
294             throw new NoSuchProviderException("Not a Store Provider: " + provider);
295         }
296         return (Store) getService(provider, null);
297     }
298 
299     /**
300      * Return a closed folder for the supplied URLName, or null if it cannot be obtained.
301      * <p/>
302      * The scheme portion of the URL is used to locate the Provider and create the Store;
303      * the returned Store is then used to obtain the folder.
304      *
305      * @param name the location of the folder
306      * @return the requested folder, or null if it is unavailable
307      * @throws NoSuchProviderException if there is no provider
308      * @throws MessagingException      if there was a problem accessing the Store
309      */
310     public Folder getFolder(URLName name) throws MessagingException {
311         Store store = getStore(name);
312         return store.getFolder(name);
313     }
314 
315     /**
316      * Return a Transport for the default protocol specified by the
317      * <code>mail.transport.protocol</code> property.
318      *
319      * @return a Transport
320      * @throws NoSuchProviderException
321      */
322     public Transport getTransport() throws NoSuchProviderException {
323         String protocol = properties.getProperty("mail.transport.protocol");
324         if (protocol == null) {
325             throw new NoSuchProviderException("mail.transport.protocol property is not set");
326         }
327         return getTransport(protocol);
328     }
329 
330     /**
331      * Return a Transport for the specified protocol.
332      *
333      * @param protocol the protocol to use
334      * @return a Transport
335      * @throws NoSuchProviderException
336      */
337     public Transport getTransport(String protocol) throws NoSuchProviderException {
338         Provider provider = getProvider(protocol);
339         return getTransport(provider);
340     }
341 
342     /**
343      * Return a transport for the protocol specified in the URL.
344      *
345      * @param name the URL whose scheme specifies the protocol
346      * @return a Transport
347      * @throws NoSuchProviderException
348      */
349     public Transport getTransport(URLName name) throws NoSuchProviderException {
350         return (Transport) getService(getProvider(name.getProtocol()), name);
351     }
352 
353     /**
354      * Return a transport for the protocol associated with the type of this address.
355      *
356      * @param address the address we are trying to deliver to
357      * @return a Transport
358      * @throws NoSuchProviderException
359      */
360     public Transport getTransport(Address address) throws NoSuchProviderException {
361         String type = address.getType();
362         // load the address map from the resource files.
363         Map addressMap = getAddressMap();
364         String protocolName = (String)addressMap.get(type);
365         if (protocolName == null) {
366             throw new NoSuchProviderException("No provider for address type " + type);
367         }
368         return getTransport(protocolName);
369     }
370 
371     /**
372      * Return the Transport specified by a Provider
373      *
374      * @param provider the defining Provider
375      * @return a Transport
376      * @throws NoSuchProviderException
377      */
378     public Transport getTransport(Provider provider) throws NoSuchProviderException {
379         return (Transport) getService(provider, null);
380     }
381 
382     /**
383      * Set the password authentication associated with a URL.
384      *
385      * @param name          the url
386      * @param authenticator the authenticator
387      */
388     public void setPasswordAuthentication(URLName name, PasswordAuthentication authenticator) {
389         if (authenticator == null) {
390             passwordAuthentications.remove(name);
391         } else {
392             passwordAuthentications.put(name, authenticator);
393         }
394     }
395 
396     /**
397      * Get the password authentication associated with a URL
398      *
399      * @param name the URL
400      * @return any authenticator for that url, or null if none
401      */
402     public PasswordAuthentication getPasswordAuthentication(URLName name) {
403         return (PasswordAuthentication) passwordAuthentications.get(name);
404     }
405 
406     /**
407      * Call back to the application supplied authenticator to get the needed username add password.
408      *
409      * @param host            the host we are trying to connect to, may be null
410      * @param port            the port on that host
411      * @param protocol        the protocol trying to be used
412      * @param prompt          a String to show as part of the prompt, may be null
413      * @param defaultUserName the default username, may be null
414      * @return the authentication information collected by the authenticator; may be null
415      */
416     public PasswordAuthentication requestPasswordAuthentication(InetAddress host, int port, String protocol, String prompt, String defaultUserName) {
417         if (authenticator == null) {
418             return null;
419         }
420         return authenticator.authenticate(host, port, protocol, prompt, defaultUserName);
421     }
422 
423     /**
424      * Return the properties object for this Session; this is a live collection.
425      *
426      * @return the properties for the Session
427      */
428     public Properties getProperties() {
429         return properties;
430     }
431 
432     /**
433      * Return the specified property.
434      *
435      * @param property the property to get
436      * @return its value, or null if not present
437      */
438     public String getProperty(String property) {
439         return getProperties().getProperty(property);
440     }
441 
442 
443     /**
444      * Add a provider to the Session managed provider list.
445      *
446      * @param provider The new provider to add.
447      */
448     public synchronized void addProvider(Provider provider) {
449         ProviderInfo info = getProviderInfo();
450         info.addProvider(provider);
451     }
452 
453 
454 
455     /**
456      * Add a mapping between an address type and a protocol used
457      * to process that address type.
458      *
459      * @param addressType
460      *                 The address type identifier.
461      * @param protocol The protocol name mapping.
462      */
463     public void setProtocolForAddress(String addressType, String protocol) {
464         Map addressMap = getAddressMap();
465 
466         // no protocol specified is a removal
467         if (protocol == null) {
468             addressMap.remove(addressType);
469         }
470         else {
471             addressMap.put(addressType, protocol);
472         }
473     }
474 
475 
476     private Service getService(Provider provider, URLName name) throws NoSuchProviderException {
477         try {
478             if (name == null) {
479                 name = new URLName(provider.getProtocol(), null, -1, null, null, null); 
480            }
481             ClassLoader cl = getClassLoader();
482             Class clazz = cl.loadClass(provider.getClassName());
483             Constructor ctr = clazz.getConstructor(PARAM_TYPES);
484             return (Service) ctr.newInstance(new Object[]{this, name});
485         } catch (ClassNotFoundException e) {
486             throw (NoSuchProviderException) new NoSuchProviderException("Unable to load class for provider: " + provider).initCause(e);
487         } catch (NoSuchMethodException e) {
488             throw (NoSuchProviderException) new NoSuchProviderException("Provider class does not have a constructor(Session, URLName): " + provider).initCause(e);
489         } catch (InstantiationException e) {
490             throw (NoSuchProviderException) new NoSuchProviderException("Unable to instantiate provider class: " + provider).initCause(e);
491         } catch (IllegalAccessException e) {
492             throw (NoSuchProviderException) new NoSuchProviderException("Unable to instantiate provider class: " + provider).initCause(e);
493         } catch (InvocationTargetException e) {
494             throw (NoSuchProviderException) new NoSuchProviderException("Exception from constructor of provider class: " + provider).initCause(e.getCause());
495         }
496     }
497 
498     private ProviderInfo getProviderInfo() {
499         ClassLoader cl = getClassLoader();
500         synchronized (providersByClassLoader) {
501             ProviderInfo info = (ProviderInfo) providersByClassLoader.get(cl);
502             if (info == null) {
503                 info = loadProviders(cl);
504             }
505             return info;
506         }
507     }
508 
509     private Map getAddressMap() {
510         ClassLoader cl = getClassLoader();
511         Map addressMap = (Map)addressMapsByClassLoader.get(cl);
512         if (addressMap == null) {
513             addressMap = loadAddressMap(cl);
514         }
515         return addressMap;
516     }
517 
518 
519     /**
520      * Resolve a class loader used to resolve context resources.  The
521      * class loader used is either a current thread context class
522      * loader (if set), the class loader used to load an authenticator
523      * we've been initialized with, or the class loader used to load
524      * this class instance (which may be a subclass of Session).
525      *
526      * @return The class loader used to load resources.
527      */
528     private ClassLoader getClassLoader() {
529         ClassLoader cl = Thread.currentThread().getContextClassLoader();
530         if (cl == null) {
531             if (authenticator != null) {
532                 cl = authenticator.getClass().getClassLoader();
533             }
534             else {
535                 cl = this.getClass().getClassLoader();
536             }
537         }
538         return cl;
539     }
540 
541     private ProviderInfo loadProviders(ClassLoader cl) {
542         // we create a merged map from reading all of the potential address map entries.  The locations
543         // searched are:
544         //   1.   java.home/lib/javamail.address.map
545         //   2. META-INF/javamail.address.map
546         //   3. META-INF/javamail.default.address.map
547         //
548         ProviderInfo info = new ProviderInfo();
549 
550         // NOTE:  Unlike the addressMap, we process these in the defined order.  The loading routine
551         // will not overwrite entries if they already exist in the map.
552 
553         try {
554             File file = new File(System.getProperty("java.home"), "lib/javamail.providers");
555             InputStream is = new FileInputStream(file);
556             try {
557                 loadProviders(info, is);
558                 if (debug) {
559                     writeDebug("Loaded lib/javamail.providers from " + file.toString());
560                 }
561             } finally{
562                 is.close();
563             }
564         } catch (SecurityException e) {
565             // ignore
566         } catch (IOException e) {
567             // ignore
568         }
569 
570         try {
571             Enumeration e = cl.getResources("META-INF/javamail.providers");
572             while (e.hasMoreElements()) {
573                 URL url = (URL) e.nextElement();
574                 if (debug) {
575                     writeDebug("Loading META-INF/javamail.providers from " + url.toString());
576                 }
577                 InputStream is = url.openStream();
578                 try {
579                     loadProviders(info, is);
580                 } finally{
581                     is.close();
582                 }
583             }
584         } catch (SecurityException e) {
585             // ignore
586         } catch (IOException e) {
587             // ignore
588         }
589 
590         try {
591             Enumeration e = cl.getResources("META-INF/javamail.default.providers");
592             while (e.hasMoreElements()) {
593                 URL url = (URL) e.nextElement();
594                 if (debug) {
595                     writeDebug("Loading javamail.default.providers from " + url.toString());
596                 }
597 
598                 InputStream is = url.openStream();
599                 try {
600                     loadProviders(info, is);
601                 } finally{
602                     is.close();
603                 }
604             }
605         } catch (SecurityException e) {
606             // ignore
607         } catch (IOException e) {
608             // ignore
609         }
610         
611         // make sure this is added to the global map.
612         providersByClassLoader.put(cl, info); 
613 
614         return info;
615     }
616 
617     private void loadProviders(ProviderInfo info, InputStream is) throws IOException {
618         BufferedReader reader = new BufferedReader(new InputStreamReader(is));
619         String line;
620         while ((line = reader.readLine()) != null) {
621             // Lines beginning with "#" are just comments.  
622             if (line.startsWith("#")) {
623                 continue; 
624             }
625             
626             StringTokenizer tok = new StringTokenizer(line, ";");
627             String protocol = null;
628             Provider.Type type = null;
629             String className = null;
630             String vendor = null;
631             String version = null;
632             while (tok.hasMoreTokens()) {
633                 String property = tok.nextToken();
634                 int index = property.indexOf('=');
635                 if (index == -1) {
636                     continue;
637                 }
638                 String key = property.substring(0, index).trim().toLowerCase();
639                 String value = property.substring(index+1).trim();
640                 if (protocol == null && "protocol".equals(key)) {
641                     protocol = value;
642                 } else if (type == null && "type".equals(key)) {
643                     if ("store".equals(value)) {
644                         type = Provider.Type.STORE;
645                     } else if ("transport".equals(value)) {
646                         type = Provider.Type.TRANSPORT;
647                     }
648                 } else if (className == null && "class".equals(key)) {
649                     className = value;
650                 } else if ("vendor".equals(key)) {
651                     vendor = value;
652                 } else if ("version".equals(key)) {
653                     version = value;
654                 }
655             }
656             if (protocol == null || type == null || className == null) {
657                 //todo should we log a warning?
658                 continue;
659             }
660 
661             if (debug) {
662                 writeDebug("DEBUG: loading new provider protocol=" + protocol + ", className=" + className + ", vendor=" + vendor + ", version=" + version);
663             }
664             Provider provider = new Provider(type, protocol, className, vendor, version);
665             // add to the info list.
666             info.addProvider(provider);
667         }
668     }
669 
670     /**
671      * Load up an address map associated with a using class loader
672      * instance.
673      *
674      * @param cl     The class loader used to resolve the address map.
675      *
676      * @return A map containing the entries associated with this classloader
677      *         instance.
678      */
679     private static Map loadAddressMap(ClassLoader cl) {
680         // we create a merged map from reading all of the potential address map entries.  The locations
681         // searched are:
682         //   1.   java.home/lib/javamail.address.map
683         //   2. META-INF/javamail.address.map
684         //   3. META-INF/javamail.default.address.map
685         //
686         // if all of the above searches fail, we just set up some "default" defaults.
687 
688         // the format of the address.map file is defined as a property file.  We can cheat and
689         // just use Properties.load() to read in the files.
690         Properties addressMap = new Properties();
691 
692         // add this to the tracking map.
693         addressMapsByClassLoader.put(cl, addressMap);
694 
695         // NOTE:  We are reading these resources in reverse order of what's cited above.  This allows
696         // user defined entries to overwrite default entries if there are similarly named items.
697 
698         try {
699             Enumeration e = cl.getResources("META-INF/javamail.default.address.map");
700             while (e.hasMoreElements()) {
701                 URL url = (URL) e.nextElement();
702                 InputStream is = url.openStream();
703                 try {
704                     // load as a property file
705                     addressMap.load(is);
706                 } finally{
707                     is.close();
708                 }
709             }
710         } catch (SecurityException e) {
711             // ignore
712         } catch (IOException e) {
713             // ignore
714         }
715 
716 
717         try {
718             Enumeration e = cl.getResources("META-INF/javamail.address.map");
719             while (e.hasMoreElements()) {
720                 URL url = (URL) e.nextElement();
721                 InputStream is = url.openStream();
722                 try {
723                     // load as a property file
724                     addressMap.load(is);
725                 } finally{
726                     is.close();
727                 }
728             }
729         } catch (SecurityException e) {
730             // ignore
731         } catch (IOException e) {
732             // ignore
733         }
734 
735 
736         try {
737             File file = new File(System.getProperty("java.home"), "lib/javamail.address.map");
738             InputStream is = new FileInputStream(file);
739             try {
740                 // load as a property file
741                 addressMap.load(is);
742             } finally{
743                 is.close();
744             }
745         } catch (SecurityException e) {
746             // ignore
747         } catch (IOException e) {
748             // ignore
749         }
750 
751         try {
752             Enumeration e = cl.getResources("META-INF/javamail.address.map");
753             while (e.hasMoreElements()) {
754                 URL url = (URL) e.nextElement();
755                 InputStream is = url.openStream();
756                 try {
757                     // load as a property file
758                     addressMap.load(is);
759                 } finally{
760                     is.close();
761                 }
762             }
763         } catch (SecurityException e) {
764             // ignore
765         } catch (IOException e) {
766             // ignore
767         }
768 
769 
770         // if unable to load anything, at least create the MimeMessage-smtp protocol mapping.
771         if (addressMap.isEmpty()) {
772             addressMap.put("rfc822", "smtp");
773         }
774 
775         return addressMap;
776     }
777 
778     /**
779      * Private convenience routine for debug output.
780      *
781      * @param msg    The message to write out to the debug stream.
782      */
783     private void writeDebug(String msg) {
784         debugOut.println(msg);
785     }
786 
787 
788     private static class ProviderInfo {
789         private final Map byClassName = new HashMap();
790         private final Map byProtocol = new HashMap();
791         private final List all = new ArrayList();
792 
793         public void addProvider(Provider provider) {
794             String className = provider.getClassName();
795 
796             if (!byClassName.containsKey(className)) {
797                 byClassName.put(className, provider);
798             }
799 
800             String protocol = provider.getProtocol();
801             if (!byProtocol.containsKey(protocol)) {
802                 byProtocol.put(protocol, provider);
803             }
804             all.add(provider);
805         }
806     }
807 }