001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smack;
019
020import java.io.BufferedReader;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.InputStreamReader;
024import java.lang.reflect.Field;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Set;
031import java.util.logging.Level;
032import java.util.logging.Logger;
033
034import javax.net.ssl.HostnameVerifier;
035
036import org.jivesoftware.smack.compression.Java7ZlibInputOutputStream;
037import org.jivesoftware.smack.compression.XMPPInputOutputStream;
038import org.jivesoftware.smack.initializer.SmackInitializer;
039import org.jivesoftware.smack.parsing.ExceptionThrowingCallback;
040import org.jivesoftware.smack.parsing.ParsingExceptionCallback;
041import org.jivesoftware.smack.util.DNSUtil;
042import org.jivesoftware.smack.util.FileUtils;
043import org.xmlpull.v1.XmlPullParserFactory;
044import org.xmlpull.v1.XmlPullParser;
045import org.xmlpull.v1.XmlPullParserException;
046
047/**
048 * Represents the configuration of Smack. The configuration is used for:
049 * <ul>
050 *      <li> Initializing classes by loading them at start-up.
051 *      <li> Getting the current Smack version.
052 *      <li> Getting and setting global library behavior, such as the period of time
053 *          to wait for replies to packets from the server. Note: setting these values
054 *          via the API will override settings in the configuration file.
055 * </ul>
056 *
057 * Configuration settings are stored in org.jivesoftware.smack/smack-config.xml.
058 * 
059 * @author Gaston Dombiak
060 */
061public final class SmackConfiguration {
062    private static final String SMACK_VERSION;
063    private static final String DEFAULT_CONFIG_FILE = "classpath:org.jivesoftware.smack/smack-config.xml";
064    
065    private static final Logger LOGGER = Logger.getLogger(SmackConfiguration.class.getName());
066
067    private static int defaultPacketReplyTimeout = 5000;
068    private static int packetCollectorSize = 5000;
069
070    private static List<String> defaultMechs = new ArrayList<String>();
071
072    private static Set<String> disabledSmackClasses = new HashSet<String>();
073
074    private final static List<XMPPInputOutputStream> compressionHandlers = new ArrayList<XMPPInputOutputStream>(2);
075
076    /**
077     * Value that indicates whether debugging is enabled. When enabled, a debug
078     * window will apear for each new connection that will contain the following
079     * information:<ul>
080     * <li> Client Traffic -- raw XML traffic generated by Smack and sent to the server.
081     * <li> Server Traffic -- raw XML traffic sent by the server to the client.
082     * <li> Interpreted Packets -- shows XML packets from the server as parsed by Smack.
083     * </ul>
084     * <p/>
085     * Debugging can be enabled by setting this field to true, or by setting the Java system
086     * property <tt>smack.debugEnabled</tt> to true. The system property can be set on the
087     * command line such as "java SomeApp -Dsmack.debugEnabled=true".
088     */
089    public static boolean DEBUG_ENABLED = false;
090
091    /**
092     * Loads the configuration from the smack-config.xml and system properties file.
093     * <p>
094     * So far this means that:
095     * 1) a set of classes will be loaded in order to execute their static init block
096     * 2) retrieve and set the current Smack release
097     * 3) set DEBUG_ENABLED
098     */
099    static {
100        String smackVersion;
101        try {
102            BufferedReader reader = new BufferedReader(new InputStreamReader(FileUtils.getStreamForUrl("classpath:org.jivesoftware.smack/version", null)));
103            smackVersion = reader.readLine();
104            try {
105                reader.close();
106            } catch (IOException e) {
107                LOGGER.log(Level.WARNING, "IOException closing stream", e);
108            }
109        } catch(Exception e) {
110            LOGGER.log(Level.SEVERE, "Could not determine Smack version", e);
111            smackVersion = "unkown";
112        }
113        SMACK_VERSION = smackVersion;
114
115        String disabledClasses = System.getProperty("smack.disabledClasses");
116        if (disabledClasses != null) {
117            String[] splitDisabledClasses = disabledClasses.split(",");
118            for (String s : splitDisabledClasses) disabledSmackClasses.add(s);
119        }
120        try {
121            FileUtils.addLines("classpath:org.jivesoftware.smack/disabledClasses", disabledSmackClasses);
122        }
123        catch (Exception e) {
124            throw new IllegalStateException(e);
125        }
126
127        try {
128            Class<?> c = Class.forName("org.jivesoftware.smack.CustomSmackConfiguration");
129            Field f = c.getField("DISABLED_SMACK_CLASSES");
130            String[] sa = (String[]) f.get(null);
131            if (sa != null)
132                for (String s : sa)
133                    disabledSmackClasses.add(s);
134        }
135        catch (ClassNotFoundException e1) {
136        }
137        catch (NoSuchFieldException e) {
138        }
139        catch (SecurityException e) {
140        }
141        catch (IllegalArgumentException e) {
142        }
143        catch (IllegalAccessException e) {
144        }
145
146        InputStream configFileStream;
147        try {
148            configFileStream = FileUtils.getStreamForUrl(DEFAULT_CONFIG_FILE, null);
149        }
150        catch (Exception e) {
151            throw new IllegalStateException(e);
152        }
153
154        try {
155            processConfigFile(configFileStream, null);
156        }
157        catch (Exception e) {
158            throw new IllegalStateException(e);
159        }
160
161        // Add the Java7 compression handler first, since it's preferred
162        compressionHandlers.add(new Java7ZlibInputOutputStream());
163
164        // Use try block since we may not have permission to get a system
165        // property (for example, when an applet).
166        try {
167            DEBUG_ENABLED = Boolean.getBoolean("smack.debugEnabled");
168        }
169        catch (Exception e) {
170            // Ignore.
171        }
172
173        // Initialize the DNS resolvers
174        DNSUtil.init();
175    }
176
177    /**
178     * The default parsing exception callback is {@link ExceptionThrowingCallback} which will
179     * throw an exception and therefore disconnect the active connection.
180     */
181    private static ParsingExceptionCallback defaultCallback = new ExceptionThrowingCallback();
182
183    private static HostnameVerifier defaultHostnameVerififer;
184
185    /**
186     * Returns the Smack version information, eg "1.3.0".
187     * 
188     * @return the Smack version information.
189     */
190    public static String getVersion() {
191        return SMACK_VERSION;
192    }
193
194    /**
195     * Returns the number of milliseconds to wait for a response from
196     * the server. The default value is 5000 ms.
197     * 
198     * @return the milliseconds to wait for a response from the server
199     */
200    public static int getDefaultPacketReplyTimeout() {
201        // The timeout value must be greater than 0 otherwise we will answer the default value
202        if (defaultPacketReplyTimeout <= 0) {
203            defaultPacketReplyTimeout = 5000;
204        }
205        return defaultPacketReplyTimeout;
206    }
207
208    /**
209     * Sets the number of milliseconds to wait for a response from
210     * the server.
211     * 
212     * @param timeout the milliseconds to wait for a response from the server
213     */
214    public static void setDefaultPacketReplyTimeout(int timeout) {
215        if (timeout <= 0) {
216            throw new IllegalArgumentException();
217        }
218        defaultPacketReplyTimeout = timeout;
219    }
220
221    /**
222     * Gets the default max size of a packet collector before it will delete 
223     * the older packets.
224     * 
225     * @return The number of packets to queue before deleting older packets.
226     */
227    public static int getPacketCollectorSize() {
228        return packetCollectorSize;
229    }
230
231    /**
232     * Sets the default max size of a packet collector before it will delete 
233     * the older packets.
234     * 
235     * @param collectorSize the number of packets to queue before deleting older packets.
236     */
237    public static void setPacketCollectorSize(int collectorSize) {
238        packetCollectorSize = collectorSize;
239    }
240    
241    /**
242     * Add a SASL mechanism to the list to be used.
243     *
244     * @param mech the SASL mechanism to be added
245     */
246    public static void addSaslMech(String mech) {
247        if(! defaultMechs.contains(mech) ) {
248            defaultMechs.add(mech);
249        }
250    }
251
252   /**
253     * Add a Collection of SASL mechanisms to the list to be used.
254     *
255     * @param mechs the Collection of SASL mechanisms to be added
256     */
257    public static void addSaslMechs(Collection<String> mechs) {
258        for(String mech : mechs) {
259            addSaslMech(mech);
260        }
261    }
262
263    /**
264     * Remove a SASL mechanism from the list to be used.
265     *
266     * @param mech the SASL mechanism to be removed
267     */
268    public static void removeSaslMech(String mech) {
269        defaultMechs.remove(mech);
270    }
271
272   /**
273     * Remove a Collection of SASL mechanisms to the list to be used.
274     *
275     * @param mechs the Collection of SASL mechanisms to be removed
276     */
277    public static void removeSaslMechs(Collection<String> mechs) {
278        defaultMechs.removeAll(mechs);
279    }
280
281    /**
282     * Returns the list of SASL mechanisms to be used. If a SASL mechanism is
283     * listed here it does not guarantee it will be used. The server may not
284     * support it, or it may not be implemented.
285     *
286     * @return the list of SASL mechanisms to be used.
287     */
288    public static List<String> getSaslMechs() {
289        return Collections.unmodifiableList(defaultMechs);
290    }
291
292    /**
293     * Set the default parsing exception callback for all newly created connections
294     *
295     * @param callback
296     * @see ParsingExceptionCallback
297     */
298    public static void setDefaultParsingExceptionCallback(ParsingExceptionCallback callback) {
299        defaultCallback = callback;
300    }
301
302    /**
303     * Returns the default parsing exception callback
304     * 
305     * @return the default parsing exception callback
306     * @see ParsingExceptionCallback
307     */
308    public static ParsingExceptionCallback getDefaultParsingExceptionCallback() {
309        return defaultCallback;
310    }
311
312    public static void addCompressionHandler(XMPPInputOutputStream xmppInputOutputStream) {
313        compressionHandlers.add(xmppInputOutputStream);
314    }
315
316    public static List<XMPPInputOutputStream> getCompresionHandlers() {
317        List<XMPPInputOutputStream> res = new ArrayList<XMPPInputOutputStream>(compressionHandlers.size());
318        for (XMPPInputOutputStream ios : compressionHandlers) {
319            if (ios.isSupported()) {
320                res.add(ios);
321            }
322        }
323        return res;
324    }
325
326    /**
327     * Set the default HostnameVerifier that will be used by XMPP connections to verify the hostname
328     * of a TLS certificate. XMPP connections are able to overwrite this settings by supplying a
329     * HostnameVerifier in their ConnecitonConfiguration with
330     * {@link ConnectionConfiguration#setHostnameVerifier(HostnameVerifier)}.
331     */
332    public static void setDefaultHostnameVerifier(HostnameVerifier verifier) {
333        defaultHostnameVerififer = verifier;
334    }
335
336    /**
337     * Get the default HostnameVerifier
338     *
339     * @return the default HostnameVerifier or <code>null</code> if none was set
340     */
341    static HostnameVerifier getDefaultHostnameVerifier() {
342        return defaultHostnameVerififer;
343    }
344
345    public static void processConfigFile(InputStream cfgFileStream,
346                    Collection<Exception> exceptions) throws Exception {
347        processConfigFile(cfgFileStream, exceptions, SmackConfiguration.class.getClassLoader());
348    }
349
350    public static void processConfigFile(InputStream cfgFileStream,
351                    Collection<Exception> exceptions, ClassLoader classLoader) throws Exception {
352        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
353        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
354        parser.setInput(cfgFileStream, "UTF-8");
355        int eventType = parser.getEventType();
356        do {
357            if (eventType == XmlPullParser.START_TAG) {
358                if (parser.getName().equals("startupClasses")) {
359                    parseClassesToLoad(parser, false, exceptions, classLoader);
360                }
361                else if (parser.getName().equals("optionalStartupClasses")) {
362                    parseClassesToLoad(parser, true, exceptions, classLoader);
363                }
364            }
365            eventType = parser.next();
366        }
367        while (eventType != XmlPullParser.END_DOCUMENT);
368        try {
369            cfgFileStream.close();
370        }
371        catch (IOException e) {
372            LOGGER.log(Level.SEVERE, "Error while closing config file input stream", e);
373        }
374    }
375
376    private static void parseClassesToLoad(XmlPullParser parser, boolean optional,
377                    Collection<Exception> exceptions, ClassLoader classLoader)
378                    throws XmlPullParserException, IOException, Exception {
379        final String startName = parser.getName();
380        int eventType;
381        String name;
382        do {
383            eventType = parser.next();
384            name = parser.getName();
385            if (eventType == XmlPullParser.START_TAG && "className".equals(name)) {
386                String classToLoad = parser.nextText();
387                if (disabledSmackClasses.contains(classToLoad)) {
388                    LOGGER.info("Not loading disabled Smack class " + classToLoad);
389                }
390                else {
391                    try {
392                        loadSmackClass(classToLoad, optional, classLoader);
393                    }
394                    catch (Exception e) {
395                        // Don't throw the exception if an exceptions collection is given, instead
396                        // record it there. This is used for unit testing purposes.
397                        if (exceptions != null) {
398                            exceptions.add(e);
399                        }
400                        else {
401                            throw e;
402                        }
403                    }
404                }
405            }
406        }
407        while (!(eventType == XmlPullParser.END_TAG && startName.equals(name)));
408    }
409
410    private static void loadSmackClass(String className, boolean optional, ClassLoader classLoader) throws Exception {
411        Class<?> initClass;
412        try {
413            // Attempt to load and initialize the class so that all static initializer blocks of
414            // class are executed
415            initClass = Class.forName(className, true, classLoader);
416        }
417        catch (ClassNotFoundException cnfe) {
418            Level logLevel;
419            if (optional) {
420                logLevel = Level.FINE;
421            }
422            else {
423                logLevel = Level.WARNING;
424            }
425            LOGGER.log(logLevel, "A startup class '" + className + "' could not be loaded.");
426            if (!optional) {
427                throw cnfe;
428            } else {
429                return;
430            }
431        }
432        if (SmackInitializer.class.isAssignableFrom(initClass)) {
433            SmackInitializer initializer = (SmackInitializer) initClass.newInstance();
434            List<Exception> exceptions = initializer.initialize();
435            if (exceptions.size() == 0) {
436                LOGGER.log(Level.FINE, "Loaded SmackInitializer " + className);
437            } else {
438                for (Exception e : exceptions) {
439                    LOGGER.log(Level.SEVERE, "Exception in loadSmackClass", e);
440                }
441            }
442        } else {
443            LOGGER.log(Level.FINE, "Loaded " + className);
444        }
445    }
446}