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