
rocks.xmpp.core.session.XmppSessionConfiguration Maven / Gradle / Ivy
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2016 Christian Schudt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package rocks.xmpp.core.session;
import rocks.xmpp.core.session.debug.XmppDebugger;
import rocks.xmpp.core.stanza.model.Presence;
import javax.xml.bind.DataBindingException;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Supplier;
/**
* A configuration for an {@link XmppSession}.
*
* Most importantly it allows you to introduce custom extensions to your {@link XmppSession}, simply by passing your JAXB annotated classes to the builder of this class
* and then {@linkplain XmppSession#XmppSession(String, XmppSessionConfiguration, ConnectionConfiguration...) use this configuration for the session}.
*
* Since creating the JAXB context is quite expensive, this class allows you to create the context once and reuse it by multiple sessions.
* You can also {@linkplain #setDefault(XmppSessionConfiguration) set} an application-wide default configuration (used by all XMPP sessions).
*
* Use the {@link #builder()} to create instances of this class:
*
*
* {@code
* XmppSessionConfiguration configuration = XmppSessionConfiguration.builder()
* .extensions(MyClass1.class, MyClass2.class)
* .debugger(ConsoleDebugger.class)
* .build();
* }
*
* This class is immutable.
*
* @author Christian Schudt
* @see XmppSession#XmppSession(String, XmppSessionConfiguration, ConnectionConfiguration...)
*/
public final class XmppSessionConfiguration {
private static final Path DEFAULT_APPLICATION_DATA_PATH;
private static volatile XmppSessionConfiguration defaultConfiguration;
static {
Path path;
String appName = "xmpp.rocks";
try {
// This is for Windows
String appData = System.getenv("APPDATA");
if (appData != null) {
path = Paths.get(appData);
} else {
// We are not on Windows, try user.home.
appData = System.getProperty("user.home");
// specifically try Mac's application data folder.
path = Paths.get(appData, "Library", "Application Support");
if (!Files.exists(path)) {
// Seems like we are not on a Mac, use user.home then.
path = Paths.get(appData);
}
}
path = path.resolve(appName);
} catch (Exception e) {
path = Paths.get(appName);
}
DEFAULT_APPLICATION_DATA_PATH = path;
}
private final JAXBContext jaxbContext;
private final XMLInputFactory xmlInputFactory;
private final XMLOutputFactory xmlOutputFactory;
private final Class extends XmppDebugger> xmppDebugger;
private final Duration defaultResponseTimeout;
private final List authenticationMechanisms;
private final Path cacheDirectory;
private final Supplier initialPresence;
private final Set extensions;
private final Locale language;
private final ReconnectionStrategy reconnectionStrategy;
/**
* Creates a configuration for an {@link XmppSession}. If you want to add custom classes to the {@link JAXBContext}, you can pass them as parameters.
*
* @param builder The builder.
*/
private XmppSessionConfiguration(Builder builder) {
this.xmppDebugger = builder.xmppDebugger;
this.defaultResponseTimeout = builder.defaultResponseTimeout;
this.authenticationMechanisms = builder.authenticationMechanisms;
this.cacheDirectory = builder.cacheDirectory;
this.initialPresence = builder.initialPresence;
this.xmlInputFactory = XMLInputFactory.newFactory();
this.xmlOutputFactory = XMLOutputFactory.newFactory();
this.language = builder.language != null ? builder.language : Locale.getDefault();
this.reconnectionStrategy = builder.reconnectionStrategy;
this.extensions = new HashSet<>();
// Find all modules, then add all extension from each module.
ServiceLoader loader = ServiceLoader.load(Module.class);
for (Module module : loader) {
extensions.addAll(module.getExtensions());
}
// Then remove any extensions, which are custom defined, e.g. a new Roster extension for the jabber:iq:roster class.
this.extensions.removeAll(builder.extensions);
// Then add the custom extensions.
this.extensions.addAll(builder.extensions);
Collection> classesToBeBound = new ArrayDeque<>();
// For each extension, get its classes in order to add them to the JAXBContext.
for (Extension extension : extensions) {
classesToBeBound.addAll(extension.getClasses());
}
try {
jaxbContext = JAXBContext.newInstance(classesToBeBound.toArray(new Class>[classesToBeBound.size()]));
} catch (JAXBException e) {
throw new DataBindingException(e);
}
}
/**
* Gets the default configuration.
*
* @return The default configuration.
*/
public static XmppSessionConfiguration getDefault() {
// Use double-checked locking idiom
if (defaultConfiguration == null) {
synchronized (XmppSessionConfiguration.class) {
if (defaultConfiguration == null) {
defaultConfiguration = XmppSessionConfiguration.builder().build();
}
}
}
return defaultConfiguration;
}
/**
* Sets the default configuration.
*
* @param configuration The default configuration.
*/
public static void setDefault(XmppSessionConfiguration configuration) {
synchronized (XmppSessionConfiguration.class) {
defaultConfiguration = configuration;
}
}
/**
* Creates a builder for this class.
*
* @return The builder.
*/
public static Builder builder() {
return new Builder();
}
/**
* Gets the JAXB context.
*
* @return The JAXB context.
*/
JAXBContext getJAXBContext() {
return jaxbContext;
}
/**
* Gets the current debugger for this session. If no debugger was set, the default debugger is the {@link rocks.xmpp.core.session.debug.ConsoleDebugger}.
*
* @return The debugger.
*/
public final Class extends XmppDebugger> getDebugger() {
return xmppDebugger;
}
/**
* Gets the response timeout.
*
* @return The response timeout.
*/
public final Duration getDefaultResponseTimeout() {
return defaultResponseTimeout;
}
/**
* Gets the preferred authentication (SASL) mechanisms.
*
* @return The mechanisms.
* @see Builder#authenticationMechanisms(String...)
*/
public final List getAuthenticationMechanisms() {
return Collections.unmodifiableList(authenticationMechanisms);
}
/**
* Gets the caching directory for directory-based caches used for:
*
* By default this directory is called xmpp.rocks
and is located in the operating system's application data folder:
*
* For Windows it is %APPDATA%
, which usually is C:\Users\{USERNAME}\AppData\Roaming
* For Mac it is ~/Library/Application Support
* Else it is the user's home directory.
*
*
* @return The directory.
*/
public final Path getCacheDirectory() {
return cacheDirectory;
}
/**
* Gets a supplier for initial presence which is sent during login.
*
* @return The initial presence supplier.
* @see 4.2. Initial Presence
*/
public final Supplier getInitialPresence() {
return initialPresence;
}
/**
* Gets the preferred or default language for any human-readable XML character data to be sent over the stream.
*
* @return The language.
* @see 4.7.4. xml:lang
*/
public final Locale getLanguage() {
return language;
}
/**
* Gets the reconnection strategy.
*
* @return The reconnection strategy.
*/
public final ReconnectionStrategy getReconnectionStrategy() {
return reconnectionStrategy;
}
final Collection getExtensions() {
return extensions;
}
/**
* Gets the XML input factory.
*
* @return the XML input factory
*/
public final XMLInputFactory getXmlInputFactory() {
return xmlInputFactory;
}
/**
* Gets the XML output factory.
*
* @return The XML output factory
*/
public final XMLOutputFactory getXmlOutputFactory() {
return xmlOutputFactory;
}
/**
* A builder to create an {@link XmppSessionConfiguration} instance.
*/
public static final class Builder {
private final Collection extensions = new ArrayDeque<>();
private Class extends XmppDebugger> xmppDebugger;
private Duration defaultResponseTimeout;
private Path cacheDirectory;
private Supplier initialPresence;
private Locale language;
private ReconnectionStrategy reconnectionStrategy;
/**
* The default preferred SASL mechanisms.
*/
private List authenticationMechanisms = Arrays.asList("SCRAM-SHA-1",
"DIGEST-MD5",
"GSSAPI",
"CRAM-MD5",
"PLAIN",
"ANONYMOUS");
private Builder() {
defaultResponseTimeout(Duration.ofSeconds(5)).cacheDirectory(DEFAULT_APPLICATION_DATA_PATH)
.initialPresence(Presence::new);
}
/**
* Sets the debugger.
*
* @param xmppDebugger The debugger or null, if you don't want to use a debugger.
* @return The debugger.
*/
public final Builder debugger(Class extends XmppDebugger> xmppDebugger) {
this.xmppDebugger = xmppDebugger;
return this;
}
/**
* Adds extensions to the session.
*
* @param extensions The extensions.
* @return The builder.
*/
public final Builder extensions(Extension... extensions) {
this.extensions.addAll(Arrays.asList(extensions));
return this;
}
/**
* Sets the default response timeout for synchronous calls, usually IQ calls.
*
* @param defaultResponseTimeout The default response timeout.
* @return The builder.
* @deprecated Use {@link #defaultResponseTimeout(Duration)}
*/
@Deprecated
public final Builder defaultResponseTimeout(int defaultResponseTimeout) {
return defaultResponseTimeout(Duration.ofMillis(defaultResponseTimeout));
}
/**
* Sets the default response timeout for synchronous calls, usually IQ calls.
*
* @param defaultResponseTimeout The default response timeout.
* @return The builder.
*/
public final Builder defaultResponseTimeout(Duration defaultResponseTimeout) {
this.defaultResponseTimeout = Objects.requireNonNull(defaultResponseTimeout);
return this;
}
/**
* Sets the preferred mechanisms used for this XMPP session.
*
*
* Any entity that will act as a SASL client or a SASL server MUST maintain an ordered list
* of its preferred SASL mechanisms according to the client or server,
* where the list is ordered according to local policy or user configuration
* (which SHOULD be in order of perceived strength to enable the strongest authentication possible).
* The initiating entity MUST maintain its own preference order independent of the preference order of the receiving entity.
* A client MUST try SASL mechanisms in its preference order.
* For example, if the server offers the ordered list "PLAIN SCRAM-SHA-1 GSSAPI" or "SCRAM-SHA-1 GSSAPI PLAIN"
* but the client's ordered list is "GSSAPI SCRAM-SHA-1",
* the client MUST try GSSAPI first and then SCRAM-SHA-1 but MUST NOT try PLAIN (since PLAIN is not on its list).
*
*
* @param authenticationMechanisms The preferred mechanisms.
* @return The builder.
*/
public final Builder authenticationMechanisms(String... authenticationMechanisms) {
this.authenticationMechanisms = Arrays.asList(authenticationMechanisms);
return this;
}
/**
* Sets the caching directory for directory-based caches used for:
*
* If you want to disable the use of directory caching, pass null.
*
* @param path The directory.
* @return The builder.
* @see #getCacheDirectory()
*/
public final Builder cacheDirectory(Path path) {
if (path != null && Files.exists(path) && !Files.isDirectory(path)) {
throw new IllegalArgumentException("path is not a directory.");
}
this.cacheDirectory = path;
return this;
}
/**
* Sets a supplier for initial presence which is sent during login. If the supplier is null or returns null, no initial presence is sent.
*
* @param presenceSupplier The presence supplier.
* @return The builder.
* @see 4.2. Initial Presence
*/
public final Builder initialPresence(Supplier presenceSupplier) {
this.initialPresence = presenceSupplier;
return this;
}
/**
* Sets the preferred or default language for any human-readable XML character data to be sent over the stream.
*
* @param language The language.
* @return The builder.
* @see 4.7.4. xml:lang
*/
public final Builder language(Locale language) {
this.language = language;
return this;
}
/**
* Sets the reconnection strategy, which determined when to reconnect after a disconnection.
*
* @param reconnectionStrategy The reconnection strategy.
* @return The builder.
* @see 3.3. Reconnection
*/
public final Builder reconnectionStrategy(ReconnectionStrategy reconnectionStrategy) {
this.reconnectionStrategy = reconnectionStrategy;
return this;
}
/**
* Builds the configuration.
*
* @return The configuration.
*/
public final XmppSessionConfiguration build() {
return new XmppSessionConfiguration(this);
}
}
}