aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--build.gradle4
-rw-r--r--libs/xmpp-addr/.gitignore1
-rw-r--r--libs/xmpp-addr/build.gradle14
-rw-r--r--libs/xmpp-addr/src/main/java/rocks/xmpp/addr/AbstractJid.java179
-rw-r--r--libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java498
-rw-r--r--libs/xmpp-addr/src/main/java/rocks/xmpp/addr/Jid.java314
-rw-r--r--libs/xmpp-addr/src/main/java/rocks/xmpp/addr/JidAdapter.java53
-rw-r--r--libs/xmpp-addr/src/main/java/rocks/xmpp/addr/MalformedJid.java131
-rw-r--r--libs/xmpp-addr/src/main/java/rocks/xmpp/addr/package-info.java31
-rw-r--r--libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/DirectoryCache.java192
-rw-r--r--libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/LruCache.java228
-rw-r--r--libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/package-info.java28
-rw-r--r--settings.gradle1
-rw-r--r--src/main/java/de/pixart/messenger/ui/util/MyLinkify.java21
14 files changed, 1692 insertions, 3 deletions
diff --git a/build.gradle b/build.gradle
index ee0e87fdc..045a38be6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -77,7 +77,7 @@ dependencies {
implementation 'pub.devrel:easypermissions:1.2.0'
implementation 'com.wefika:flowlayout:0.4.1'
implementation 'com.googlecode.ez-vcard:ez-vcard:0.10.3'
- implementation 'rocks.xmpp:xmpp-addr:0.8.0'
+ implementation project(':libs:xmpp-addr')
implementation 'org.hsluv:hsluv:0.2'
implementation 'org.conscrypt:conscrypt-android:1.3.0'
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.15'
@@ -94,7 +94,7 @@ android {
compileSdkVersion 28
defaultConfig {
- minSdkVersion 16
+ minSdkVersion 18
targetSdkVersion 28
versionCode 249
diff --git a/libs/xmpp-addr/.gitignore b/libs/xmpp-addr/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/libs/xmpp-addr/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/libs/xmpp-addr/build.gradle b/libs/xmpp-addr/build.gradle
new file mode 100644
index 000000000..2d30752c4
--- /dev/null
+++ b/libs/xmpp-addr/build.gradle
@@ -0,0 +1,14 @@
+apply plugin: 'java-library'
+
+repositories {
+ google()
+ jcenter()
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'rocks.xmpp:precis:1.0.0'
+}
+
+sourceCompatibility = "8"
+targetCompatibility = "8"
diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/AbstractJid.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/AbstractJid.java
new file mode 100644
index 000000000..963c3a491
--- /dev/null
+++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/AbstractJid.java
@@ -0,0 +1,179 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2014-2017 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.addr;
+
+import java.text.Collator;
+import java.util.Arrays;
+
+/**
+ * Abstract Jid implementation for both full and bare JIDs.
+ *
+ * @author Christian Schudt
+ */
+abstract class AbstractJid implements Jid {
+
+ /**
+ * Checks if the JID is a full JID.
+ * <blockquote>
+ * <p>The term "full JID" refers to an XMPP address of the form &lt;localpart@domainpart/resourcepart&gt; (for a particular authorized client or device associated with an account) or of the form &lt;domainpart/resourcepart&gt; (for a particular resource or script associated with a server).</p>
+ * </blockquote>
+ *
+ * @return True, if the JID is a full JID; otherwise false.
+ */
+ @Override
+ public final boolean isFullJid() {
+ return getResource() != null;
+ }
+
+ /**
+ * Checks if the JID is a bare JID.
+ * <blockquote>
+ * <p>The term "bare JID" refers to an XMPP address of the form &lt;localpart@domainpart&gt; (for an account at a server) or of the form &lt;domainpart&gt; (for a server).</p>
+ * </blockquote>
+ *
+ * @return True, if the JID is a bare JID; otherwise false.
+ */
+ @Override
+ public final boolean isBareJid() {
+ return getResource() == null;
+ }
+
+ @Override
+ public final boolean isDomainJid() {
+ return getLocal() == null;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof Jid)) {
+ return false;
+ }
+ Jid other = (Jid) o;
+
+ return (getLocal() == other.getLocal() || getLocal() != null && getLocal().equals(other.getLocal()))
+ && (getDomain() == other.getDomain() || getDomain() != null && getDomain().equals(other.getDomain()))
+ && (getResource() == other.getResource() || getResource() != null && getResource().equals(other.getResource()));
+ }
+
+ @Override
+ public final int hashCode() {
+ return Arrays.hashCode(new String[]{getLocal(), getDomain(), getResource()});
+ }
+
+ /**
+ * Compares this JID with another JID. First domain parts are compared. If these are equal, local parts are compared
+ * and if these are equal, too, resource parts are compared.
+ *
+ * @param o The other JID.
+ * @return The comparison result.
+ */
+ @Override
+ public final int compareTo(Jid o) {
+
+ if (this == o) {
+ return 0;
+ }
+
+ if (o != null) {
+ final Collator collator = Collator.getInstance();
+ int result;
+ // First compare domain parts.
+ if (getDomain() != null) {
+ result = o.getDomain() != null ? collator.compare(getDomain(), o.getDomain()) : -1;
+ } else {
+ result = o.getDomain() != null ? 1 : 0;
+ }
+ // If the domains are equal, compare local parts.
+ if (result == 0) {
+ if (getLocal() != null) {
+ // If this local part is not null, but the other is null, move this down (1).
+ result = o.getLocal() != null ? collator.compare(getLocal(), o.getLocal()) : 1;
+ } else {
+ // If this local part is null, but the other is not, move this up (-1).
+ result = o.getLocal() != null ? -1 : 0;
+ }
+ }
+ // If the local parts are equal, compare resource parts.
+ if (result == 0) {
+ if (getResource() != null) {
+ // If this resource part is not null, but the other is null, move this down (1).
+ return o.getResource() != null ? collator.compare(getResource(), o.getResource()) : 1;
+ } else {
+ // If this resource part is null, but the other is not, move this up (-1).
+ return o.getResource() != null ? -1 : 0;
+ }
+ }
+ return result;
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public final int length() {
+ return toString().length();
+ }
+
+ @Override
+ public final char charAt(int index) {
+ return toString().charAt(index);
+ }
+
+ @Override
+ public final CharSequence subSequence(int start, int end) {
+ return toString().subSequence(start, end);
+ }
+
+ /**
+ * Returns the JID in its string representation, i.e. [ localpart "@" ] domainpart [ "/" resourcepart ].
+ *
+ * @return The JID.
+ * @see #toEscapedString()
+ */
+ @Override
+ public final String toString() {
+ return toString(getLocal(), getDomain(), getResource());
+ }
+
+ @Override
+ public final String toEscapedString() {
+ return toString(getEscapedLocal(), getDomain(), getResource());
+ }
+
+ static String toString(String local, String domain, String resource) {
+ StringBuilder sb = new StringBuilder();
+ if (local != null) {
+ sb.append(local).append('@');
+ }
+ sb.append(domain);
+ if (resource != null) {
+ sb.append('/').append(resource);
+ }
+ return sb.toString();
+ }
+}
diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java
new file mode 100644
index 000000000..24130fd1b
--- /dev/null
+++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java
@@ -0,0 +1,498 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2014-2017 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.addr;
+
+import rocks.xmpp.precis.PrecisProfile;
+import rocks.xmpp.precis.PrecisProfiles;
+import rocks.xmpp.util.cache.LruCache;
+
+import java.net.IDN;
+import java.nio.charset.Charset;
+import java.text.Normalizer;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The implementation of the JID as described in <a href="https://tools.ietf.org/html/rfc7622">Extensible Messaging and Presence Protocol (XMPP): Address Format</a>.
+ * <p>
+ * This class is thread-safe and immutable.
+ *
+ * @author Christian Schudt
+ * @see <a href="https://tools.ietf.org/html/rfc7622">RFC 7622 - Extensible Messaging and Presence Protocol (XMPP): Address Format</a>
+ */
+final class FullJid extends AbstractJid {
+
+ /**
+ * Escapes all disallowed characters and also backslash, when followed by a defined hex code for escaping. See 4. Business Rules.
+ */
+ private static final Pattern ESCAPE_PATTERN = Pattern.compile("[ \"&'/:<>@]|\\\\(?=20|22|26|27|2f|3a|3c|3e|40|5c)");
+
+ private static final Pattern UNESCAPE_PATTERN = Pattern.compile("\\\\(20|22|26|27|2f|3a|3c|3e|40|5c)");
+
+ private static final Pattern JID = Pattern.compile("^((.*?)@)?([^/@]+)(/(.*))?$");
+
+ private static final IDNProfile IDN_PROFILE = new IDNProfile();
+
+ /**
+ * Whenever dots are used as label separators, the following characters MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61 (halfwidth ideographic full stop).
+ */
+ private static final String DOTS = "[.\u3002\uFF0E\uFF61]";
+
+ /**
+ * Label separators for domain labels, which should be mapped to "." (dot): IDEOGRAPHIC FULL STOP character (U+3002)
+ */
+ private static final Pattern LABEL_SEPARATOR = Pattern.compile(DOTS);
+
+ private static final Pattern LABEL_SEPARATOR_FINAL = Pattern.compile(DOTS + "$");
+
+ /**
+ * Caches the escaped JIDs.
+ */
+ private static final Map<CharSequence, Jid> ESCAPED_CACHE = new LruCache<>(5000);
+
+ /**
+ * Caches the unescaped JIDs.
+ */
+ private static final Map<CharSequence, Jid> UNESCAPED_CACHE = new LruCache<>(5000);
+
+ private static final long serialVersionUID = -3824234106101731424L;
+
+ private final String escapedLocal;
+
+ private final String local;
+
+ private final String domain;
+
+ private final String resource;
+
+ private final Jid bareJid;
+
+ /**
+ * Creates a full JID with local, domain and resource part.
+ *
+ * @param local The local part.
+ * @param domain The domain part.
+ * @param resource The resource part.
+ */
+ FullJid(CharSequence local, CharSequence domain, CharSequence resource) {
+ this(local, domain, resource, false, null);
+ }
+
+ private FullJid(final CharSequence local, final CharSequence domain, final CharSequence resource, final boolean doUnescape, Jid bareJid) {
+ final String enforcedLocalPart;
+ final String enforcedDomainPart;
+ final String enforcedResource;
+
+ final String unescapedLocalPart;
+
+ if (domain == null) {
+ throw new NullPointerException();
+ }
+
+ if (doUnescape) {
+ unescapedLocalPart = unescape(local);
+ } else {
+ unescapedLocalPart = local != null ? local.toString() : null;
+ }
+
+ // Escape the local part, so that disallowed characters like the space characters pass the UsernameCaseMapped profile.
+ final String escapedLocalPart = escape(unescapedLocalPart);
+
+ // If the domainpart includes a final character considered to be a label
+ // separator (dot) by [RFC1034], this character MUST be stripped from
+ // the domainpart before the JID of which it is a part is used for the
+ // purpose of routing an XML stanza, comparing against another JID, or
+ // constructing an XMPP URI or IRI [RFC5122]. In particular, such a
+ // character MUST be stripped before any other canonicalization steps
+ // are taken.
+ // Also validate, that the domain name can be converted to ASCII, i.e. validate the domain name (e.g. must not start with "_").
+ final String strDomain = IDN.toASCII(LABEL_SEPARATOR_FINAL.matcher(domain).replaceAll(""), IDN.USE_STD3_ASCII_RULES);
+ enforcedLocalPart = escapedLocalPart != null ? PrecisProfiles.USERNAME_CASE_MAPPED.enforce(escapedLocalPart) : null;
+ enforcedResource = resource != null ? PrecisProfiles.OPAQUE_STRING.enforce(resource) : null;
+ // See https://tools.ietf.org/html/rfc5895#section-2
+ enforcedDomainPart = IDN_PROFILE.enforce(strDomain);
+
+ validateLength(enforcedLocalPart, "local");
+ validateLength(enforcedResource, "resource");
+ validateDomain(strDomain);
+
+ this.local = unescape(enforcedLocalPart);
+ this.escapedLocal = enforcedLocalPart;
+ this.domain = enforcedDomainPart;
+ this.resource = enforcedResource;
+ if (bareJid != null) {
+ this.bareJid = bareJid;
+ } else {
+ this.bareJid = isBareJid() ? this : new AbstractJid() {
+
+ @Override
+ public Jid asBareJid() {
+ return this;
+ }
+
+ @Override
+ public Jid withLocal(CharSequence local) {
+ if (local == this.getLocal() || local != null && local.equals(this.getLocal())) {
+ return this;
+ }
+ return new FullJid(local, getDomain(), getResource(), false, null);
+ }
+
+ @Override
+ public Jid withResource(CharSequence resource) {
+ if (resource == this.getResource() || resource != null && resource.equals(this.getResource())) {
+ return this;
+ }
+ return new FullJid(getLocal(), getDomain(), resource, false, asBareJid());
+ }
+
+ @Override
+ public Jid atSubdomain(CharSequence subdomain) {
+ if (subdomain == null) {
+ throw new NullPointerException();
+ }
+ return new FullJid(getLocal(), subdomain + "." + getDomain(), getResource(), false, null);
+ }
+
+ @Override
+ public String getLocal() {
+ return FullJid.this.getLocal();
+ }
+
+ @Override
+ public String getEscapedLocal() {
+ return FullJid.this.getEscapedLocal();
+ }
+
+ @Override
+ public String getDomain() {
+ return FullJid.this.getDomain();
+ }
+
+ @Override
+ public String getResource() {
+ return null;
+ }
+ };
+ }
+ }
+
+ /**
+ * Creates a JID from a string. The format must be
+ * <blockquote><p>[ localpart "@" ] domainpart [ "/" resourcepart ]</p></blockquote>
+ *
+ * @param jid The JID.
+ * @param doUnescape If the jid parameter will be unescaped.
+ * @return The JID.
+ * @throws NullPointerException If the jid is null.
+ * @throws IllegalArgumentException If the jid could not be parsed or is not valid.
+ * @see <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>
+ */
+ static Jid of(String jid, final boolean doUnescape) {
+ if (jid == null) {
+ throw new NullPointerException("jid must not be null.");
+ }
+
+ jid = jid.trim();
+
+ if (jid.isEmpty()) {
+ throw new IllegalArgumentException("jid must not be empty.");
+ }
+
+ Jid result;
+ if (doUnescape) {
+ result = UNESCAPED_CACHE.get(jid);
+ } else {
+ result = ESCAPED_CACHE.get(jid);
+ }
+
+ if (result != null) {
+ return result;
+ }
+
+ Matcher matcher = JID.matcher(jid);
+ if (matcher.matches()) {
+ Jid jidValue = new FullJid(matcher.group(2), matcher.group(3), matcher.group(5), doUnescape, null);
+ if (doUnescape) {
+ UNESCAPED_CACHE.put(jid, jidValue);
+ } else {
+ ESCAPED_CACHE.put(jid, jidValue);
+ }
+ return jidValue;
+ } else {
+ throw new IllegalArgumentException("Could not parse JID: " + jid);
+ }
+ }
+
+ /**
+ * Escapes a local part. The characters {@code "&'/:<>@} (+ whitespace) are replaced with their respective escape characters.
+ *
+ * @param localPart The local part.
+ * @return The escaped local part or null.
+ * @see <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>
+ */
+ private static String escape(final CharSequence localPart) {
+ if (localPart != null) {
+ final Matcher matcher = ESCAPE_PATTERN.matcher(localPart);
+ final StringBuffer sb = new StringBuffer();
+ while (matcher.find()) {
+ matcher.appendReplacement(sb, "\\\\" + Integer.toHexString(matcher.group().charAt(0)));
+ }
+ matcher.appendTail(sb);
+ return sb.toString();
+ }
+ return null;
+ }
+
+ private static String unescape(final CharSequence localPart) {
+ if (localPart != null) {
+ final Matcher matcher = UNESCAPE_PATTERN.matcher(localPart);
+ final StringBuffer sb = new StringBuffer();
+ while (matcher.find()) {
+ final char c = (char) Integer.parseInt(matcher.group(1), 16);
+ if (c == '\\') {
+ matcher.appendReplacement(sb, "\\\\");
+ } else {
+ matcher.appendReplacement(sb, String.valueOf(c));
+ }
+ }
+ matcher.appendTail(sb);
+ return sb.toString();
+ }
+ return null;
+ }
+
+ private static void validateDomain(String domain) {
+ if (domain == null) {
+ throw new NullPointerException("domain must not be null.");
+ }
+ if (domain.contains("@")) {
+ // Prevent misuse of API.
+ throw new IllegalArgumentException("domain must not contain a '@' sign");
+ }
+ validateLength(domain, "domain");
+ }
+
+ /**
+ * Validates that the length of a local, domain or resource part is not longer than 1023 characters.
+ *
+ * @param value The value.
+ * @param part The part, only used to produce an exception message.
+ */
+ private static void validateLength(CharSequence value, CharSequence part) {
+ if (value != null) {
+ if (value.length() == 0) {
+ throw new IllegalArgumentException(part + " must not be empty.");
+ }
+ if (value.toString().getBytes(Charset.forName("UTF-8")).length > 1023) {
+ throw new IllegalArgumentException(part + " must not be greater than 1023 bytes.");
+ }
+ }
+ }
+
+ /**
+ * Converts this JID into a bare JID, i.e. removes the resource part.
+ * <blockquote>
+ * <p>The term "bare JID" refers to an XMPP address of the form &lt;localpart@domainpart&gt; (for an account at a server) or of the form &lt;domainpart&gt; (for a server).</p>
+ * </blockquote>
+ *
+ * @return The bare JID.
+ * @see #withResource(CharSequence)
+ */
+ @Override
+ public final Jid asBareJid() {
+ return bareJid;
+ }
+
+ /**
+ * Gets the local part of the JID, also known as the name or node.
+ * <blockquote>
+ * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.3">3.3. Localpart</a></cite></p>
+ * <p>The localpart of a JID is an optional identifier placed before the
+ * domainpart and separated from the latter by the '@' character.
+ * Typically, a localpart uniquely identifies the entity requesting and
+ * using network access provided by a server (i.e., a local account),
+ * although it can also represent other kinds of entities (e.g., a
+ * chatroom associated with a multi-user chat service [XEP-0045]). The
+ * entity represented by an XMPP localpart is addressed within the
+ * context of a specific domain (i.e., &lt;localpart@domainpart&gt;).</p>
+ * </blockquote>
+ *
+ * @return The local part or null.
+ */
+ @Override
+ public final String getLocal() {
+ return local;
+ }
+
+ @Override
+ public final String getEscapedLocal() {
+ return escapedLocal;
+ }
+
+ /**
+ * Gets the domain part.
+ * <blockquote>
+ * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.2">3.2. Domainpart</a></cite></p>
+ * <p>The domainpart is the primary identifier and is the only REQUIRED
+ * element of a JID (a mere domainpart is a valid JID). Typically,
+ * a domainpart identifies the "home" server to which clients connect
+ * for XML routing and data management functionality.</p>
+ * </blockquote>
+ *
+ * @return The domain part.
+ */
+ @Override
+ public final String getDomain() {
+ return domain;
+ }
+
+ /**
+ * Gets the resource part.
+ * <blockquote>
+ * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.4">3.4. Resourcepart</a></cite></p>
+ * <p>The resourcepart of a JID is an optional identifier placed after the
+ * domainpart and separated from the latter by the '/' character. A
+ * resourcepart can modify either a &lt;localpart@domainpart&gt; address or a
+ * mere &lt;domainpart&gt; address. Typically, a resourcepart uniquely
+ * identifies a specific connection (e.g., a device or location) or
+ * object (e.g., an occupant in a multi-user chatroom [XEP-0045])
+ * belonging to the entity associated with an XMPP localpart at a domain
+ * (i.e., &lt;localpart@domainpart/resourcepart&gt;).</p>
+ * </blockquote>
+ *
+ * @return The resource part or null.
+ */
+ @Override
+ public final String getResource() {
+ return resource;
+ }
+
+ /**
+ * Creates a new JID with a new local part and the same domain and resource part of the current JID.
+ *
+ * @param local The local part.
+ * @return The JID with a new local part.
+ * @throws IllegalArgumentException If the local is not a valid local part.
+ * @see #withResource(CharSequence)
+ */
+ @Override
+ public final Jid withLocal(CharSequence local) {
+ if (local == this.getLocal() || local != null && local.equals(this.getLocal())) {
+ return this;
+ }
+ return new FullJid(local, getDomain(), getResource(), false, null);
+ }
+
+ /**
+ * Creates a new full JID with a resource and the same local and domain part of the current JID.
+ *
+ * @param resource The resource.
+ * @return The full JID with a resource.
+ * @throws IllegalArgumentException If the resource is not a valid resource part.
+ * @see #asBareJid()
+ * @see #withLocal(CharSequence)
+ */
+ @Override
+ public final Jid withResource(CharSequence resource) {
+ if (resource == this.getResource() || resource != null && resource.equals(this.getResource())) {
+ return this;
+ }
+ return new FullJid(getLocal(), getDomain(), resource, false, asBareJid());
+ }
+
+ /**
+ * Creates a new JID at a subdomain and at the same domain as this JID.
+ *
+ * @param subdomain The subdomain.
+ * @return The JID at a subdomain.
+ * @throws NullPointerException If subdomain is null.
+ * @throws IllegalArgumentException If subdomain is not a valid subdomain name.
+ */
+ @Override
+ public final Jid atSubdomain(CharSequence subdomain) {
+ if (subdomain != null) {
+ throw new NullPointerException();
+ }
+ return new FullJid(getLocal(), subdomain + "." + getDomain(), getResource(), false, null);
+ }
+
+ /**
+ * A profile for applying the rules for IDN as in RFC 5895. Although IDN doesn't use Precis, it's still very similar so that we can use the base class.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc5895#section-2">RFC 5895</a>
+ */
+ private static final class IDNProfile extends PrecisProfile {
+
+ private IDNProfile() {
+ super(false);
+ }
+
+ @Override
+ public String prepare(CharSequence input) {
+ return IDN.toUnicode(input.toString(), IDN.USE_STD3_ASCII_RULES);
+ }
+
+ @Override
+ public String enforce(CharSequence input) {
+ // 4. Map IDEOGRAPHIC FULL STOP character (U+3002) to dot.
+ return applyAdditionalMappingRule(
+ // 3. All characters are mapped using Unicode Normalization Form C (NFC).
+ applyNormalizationRule(
+ // 2. Fullwidth and halfwidth characters (those defined with
+ // Decomposition Types <wide> and <narrow>) are mapped to their
+ // decomposition mappings
+ applyWidthMappingRule(
+ // 1. Uppercase characters are mapped to their lowercase equivalents
+ applyCaseMappingRule(prepare(input))))).toString();
+ }
+
+ @Override
+ protected CharSequence applyWidthMappingRule(CharSequence charSequence) {
+ return widthMap(charSequence);
+ }
+
+ @Override
+ protected CharSequence applyAdditionalMappingRule(CharSequence charSequence) {
+ return LABEL_SEPARATOR.matcher(charSequence).replaceAll(".");
+ }
+
+ @Override
+ protected CharSequence applyCaseMappingRule(CharSequence charSequence) {
+ return charSequence.toString().toLowerCase();
+ }
+
+ @Override
+ protected CharSequence applyNormalizationRule(CharSequence charSequence) {
+ return Normalizer.normalize(charSequence, Normalizer.Form.NFC);
+ }
+
+ @Override
+ protected CharSequence applyDirectionalityRule(CharSequence charSequence) {
+ return charSequence;
+ }
+ }
+}
diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/Jid.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/Jid.java
new file mode 100644
index 000000000..34c33d93a
--- /dev/null
+++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/Jid.java
@@ -0,0 +1,314 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2014-2017 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.addr;
+
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import java.io.Serializable;
+
+/**
+ * Represents the JID as described in <a href="https://tools.ietf.org/html/rfc7622">Extensible Messaging and Presence Protocol (XMPP): Address Format</a>.
+ * <p>
+ * A JID consists of three parts:
+ * <p>
+ * [ localpart "@" ] domainpart [ "/" resourcepart ]
+ * </p>
+ * The easiest way to create a JID is to use the {@link #of(CharSequence)} method:
+ * ```java
+ * Jid jid = Jid.of("juliet@capulet.lit/balcony");
+ * ```
+ * You can then get the parts from it via the respective methods:
+ * ```java
+ * String local = jid.getLocal(); // juliet
+ * String domain = jid.getDomain(); // capulet.lit
+ * String resource = jid.getResource(); // balcony
+ * ```
+ * Implementations of this interface should override <code>equals()</code> and <code>hashCode()</code>, so that different instances with the same value are equal:
+ * ```java
+ * Jid.of("romeo@capulet.lit/balcony").equals(Jid.of("romeo@capulet.lit/balcony")); // true
+ * ```
+ * The default implementation of this class also supports <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>, i.e.
+ * ```java
+ * Jid.of("d'artagnan@musketeers.lit")
+ * ```
+ * is escaped as <code>d\\27artagnan@musketeers.lit</code>.
+ * <p>
+ * Implementations of this interface should be thread-safe and immutable.
+ *
+ * @author Christian Schudt
+ * @see <a href="https://tools.ietf.org/html/rfc7622">RFC 7622 - Extensible Messaging and Presence Protocol (XMPP): Address Format</a>
+ */
+@XmlJavaTypeAdapter(JidAdapter.class)
+public interface Jid extends Comparable<Jid>, Serializable, CharSequence {
+
+ /**
+ * The maximal length of a full JID, which is 3071.
+ * <blockquote>
+ * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.1">3.1. Fundamentals</a></cite></p>
+ * <p>Each allowable portion of a JID (localpart, domainpart, and
+ * resourcepart) is 1 to 1023 octets in length, resulting in a maximum
+ * total size (including the '@' and '/' separators) of 3071 octets.
+ * </p>
+ * </blockquote>
+ * Note that the length is based on bytes, not characters.
+ *
+ * @see #MAX_BARE_JID_LENGTH
+ */
+ int MAX_FULL_JID_LENGTH = 3071;
+
+ /**
+ * The maximal length of a bare JID, which is 2047 (1023 + 1 + 1023).
+ * Note that the length is based on bytes, not characters.
+ *
+ * @see #MAX_FULL_JID_LENGTH
+ */
+ int MAX_BARE_JID_LENGTH = 2047;
+
+ /**
+ * The service discovery feature used for determining support of JID escaping (<code>jid\20escaping</code>).
+ */
+ String ESCAPING_FEATURE = "jid\\20escaping";
+
+ /**
+ * Returns a full JID with a domain and resource part, e.g. <code>capulet.com/balcony</code>
+ *
+ * @param local The local part.
+ * @param domain The domain.
+ * @param resource The resource part.
+ * @return The JID.
+ * @throws NullPointerException If the domain is null.
+ * @throws IllegalArgumentException If the domain, local or resource part are not valid.
+ */
+ static Jid of(CharSequence local, CharSequence domain, CharSequence resource) {
+ return new FullJid(local, domain, resource);
+ }
+
+ /**
+ * Creates a bare JID with only the domain part, e.g. <code>capulet.com</code>
+ *
+ * @param domain The domain.
+ * @return The JID.
+ * @throws NullPointerException If the domain is null.
+ * @throws IllegalArgumentException If the domain or local part are not valid.
+ */
+ static Jid ofDomain(CharSequence domain) {
+ return new FullJid(null, domain, null);
+ }
+
+ /**
+ * Creates a bare JID with a local and domain part, e.g. <code>juliet@capulet.com</code>
+ *
+ * @param local The local part.
+ * @param domain The domain.
+ * @return The JID.
+ * @throws NullPointerException If the domain is null.
+ * @throws IllegalArgumentException If the domain or local part are not valid.
+ */
+ static Jid ofLocalAndDomain(CharSequence local, CharSequence domain) {
+ return new FullJid(local, domain, null);
+ }
+
+ /**
+ * Creates a full JID with a domain and resource part, e.g. <code>capulet.com/balcony</code>
+ *
+ * @param domain The domain.
+ * @param resource The resource part.
+ * @return The JID.
+ * @throws NullPointerException If the domain is null.
+ * @throws IllegalArgumentException If the domain or resource are not valid.
+ */
+ static Jid ofDomainAndResource(CharSequence domain, CharSequence resource) {
+ return new FullJid(null, domain, resource);
+ }
+
+ /**
+ * Creates a JID from an unescaped string. The format must be
+ * <blockquote><p>[ localpart "@" ] domainpart [ "/" resourcepart ]</p></blockquote>
+ * The input string will be escaped.
+ *
+ * @param jid The JID.
+ * @return The JID.
+ * @throws NullPointerException If the jid is null.
+ * @throws IllegalArgumentException If the jid could not be parsed or is not valid.
+ * @see <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>
+ */
+ static Jid of(CharSequence jid) {
+ if (jid instanceof Jid) {
+ return (Jid) jid;
+ }
+ return FullJid.of(jid.toString(), false);
+ }
+
+ /**
+ * Creates a JID from a escaped JID string. The format must be
+ * <blockquote><p>[ localpart "@" ] domainpart [ "/" resourcepart ]</p></blockquote>
+ * This method should be used, when parsing JIDs from the XMPP stream.
+ *
+ * @param jid The JID.
+ * @return The JID.
+ * @throws NullPointerException If the jid is null.
+ * @throws IllegalArgumentException If the jid could not be parsed or is not valid.
+ * @see <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>
+ */
+ static Jid ofEscaped(CharSequence jid) {
+ return FullJid.of(jid.toString(), true);
+ }
+
+ /**
+ * Checks if the JID is a full JID.
+ * <blockquote>
+ * <p>The term "full JID" refers to an XMPP address of the form &lt;localpart@domainpart/resourcepart&gt; (for a particular authorized client or device associated with an account) or of the form &lt;domainpart/resourcepart&gt; (for a particular resource or script associated with a server).</p>
+ * </blockquote>
+ *
+ * @return True, if the JID is a full JID; otherwise false.
+ */
+ boolean isFullJid();
+
+ /**
+ * Checks if the JID is a bare JID.
+ * <blockquote>
+ * <p>The term "bare JID" refers to an XMPP address of the form &lt;localpart@domainpart&gt; (for an account at a server) or of the form &lt;domainpart&gt; (for a server).</p>
+ * </blockquote>
+ *
+ * @return True, if the JID is a bare JID; otherwise false.
+ */
+ boolean isBareJid();
+
+ /**
+ * Checks if the JID is a domain JID, i.e. if it has no local part.
+ *
+ * @return True, if the JID is a domain JID, i.e. if it has no local part.
+ */
+ boolean isDomainJid();
+
+ /**
+ * Gets the bare JID representation of this JID, i.e. removes the resource part.
+ * <blockquote>
+ * <p>The term "bare JID" refers to an XMPP address of the form &lt;localpart@domainpart&gt; (for an account at a server) or of the form &lt;domainpart&gt; (for a server).</p>
+ * </blockquote>
+ *
+ * @return The bare JID.
+ * @see #withResource(CharSequence)
+ */
+ Jid asBareJid();
+
+ /**
+ * Creates a new JID with a new local part and the same domain and resource part of the current JID.
+ *
+ * @param local The local part.
+ * @return The JID with a new local part.
+ * @throws IllegalArgumentException If the local is not a valid local part.
+ * @see #withResource(CharSequence)
+ */
+ Jid withLocal(CharSequence local);
+
+ /**
+ * Creates a new full JID with a resource and the same local and domain part of the current JID.
+ *
+ * @param resource The resource.
+ * @return The full JID with a resource.
+ * @throws IllegalArgumentException If the resource is not a valid resource part.
+ * @see #asBareJid()
+ * @see #withLocal(CharSequence)
+ */
+ Jid withResource(CharSequence resource);
+
+ /**
+ * Creates a new JID at a subdomain and at the same domain as this JID.
+ *
+ * @param subdomain The subdomain.
+ * @return The JID at a subdomain.
+ * @throws NullPointerException If subdomain is null.
+ * @throws IllegalArgumentException If subdomain is not a valid subdomain name.
+ */
+ Jid atSubdomain(CharSequence subdomain);
+
+ /**
+ * Gets the local part of the JID, also known as the name or node.
+ * <blockquote>
+ * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.3">3.3. Localpart</a></cite></p>
+ * <p>The localpart of a JID is an optional identifier placed before the
+ * domainpart and separated from the latter by the '@' character.
+ * Typically, a localpart uniquely identifies the entity requesting and
+ * using network access provided by a server (i.e., a local account),
+ * although it can also represent other kinds of entities (e.g., a
+ * chatroom associated with a multi-user chat service [XEP-0045]). The
+ * entity represented by an XMPP localpart is addressed within the
+ * context of a specific domain (i.e., &lt;localpart@domainpart&gt;).</p>
+ * </blockquote>
+ *
+ * @return The local part or null.
+ * @see #getEscapedLocal()
+ */
+ String getLocal();
+
+ /**
+ * Gets the escaped local part of the JID.
+ *
+ * @return The escaped local part or null.
+ * @see #getLocal()
+ * @since 0.8.0
+ */
+ String getEscapedLocal();
+
+ /**
+ * Gets the domain part.
+ * <blockquote>
+ * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.2">3.2. Domainpart</a></cite></p>
+ * <p>The domainpart is the primary identifier and is the only REQUIRED
+ * element of a JID (a mere domainpart is a valid JID). Typically,
+ * a domainpart identifies the "home" server to which clients connect
+ * for XML routing and data management functionality.</p>
+ * </blockquote>
+ *
+ * @return The domain part.
+ */
+ String getDomain();
+
+ /**
+ * Gets the resource part.
+ * <blockquote>
+ * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.4">3.4. Resourcepart</a></cite></p>
+ * <p>The resourcepart of a JID is an optional identifier placed after the
+ * domainpart and separated from the latter by the '/' character. A
+ * resourcepart can modify either a &lt;localpart@domainpart&gt; address or a
+ * mere &lt;domainpart&gt; address. Typically, a resourcepart uniquely
+ * identifies a specific connection (e.g., a device or location) or
+ * object (e.g., an occupant in a multi-user chatroom [XEP-0045])
+ * belonging to the entity associated with an XMPP localpart at a domain
+ * (i.e., &lt;localpart@domainpart/resourcepart&gt;).</p>
+ * </blockquote>
+ *
+ * @return The resource part or null.
+ */
+ String getResource();
+
+ /**
+ * Returns the JID in escaped form as described in <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>.
+ *
+ * @return The escaped JID.
+ * @see #toString()
+ */
+ String toEscapedString();
+}
diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/JidAdapter.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/JidAdapter.java
new file mode 100644
index 000000000..dcd981e68
--- /dev/null
+++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/JidAdapter.java
@@ -0,0 +1,53 @@
+/*
+ * 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.addr;
+
+import javax.xml.bind.annotation.adapters.XmlAdapter;
+
+/**
+ * Converts a String representation of a JID to JID object and vice a versa.
+ */
+final class JidAdapter extends XmlAdapter<String, Jid> {
+
+ @Override
+ public Jid unmarshal(String v) {
+ if (v != null) {
+ try {
+ return Jid.ofEscaped(v);
+ } catch (Exception e) {
+ return MalformedJid.of(v, e);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String marshal(Jid v) {
+ if (v != null) {
+ return v.toEscapedString();
+ }
+ return null;
+ }
+}
diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/MalformedJid.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/MalformedJid.java
new file mode 100644
index 000000000..f8605bfc1
--- /dev/null
+++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/MalformedJid.java
@@ -0,0 +1,131 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2014-2017 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.addr;
+
+/**
+ * Represents a malformed JID in order to handle the <code>jid-malformed</code> error.
+ * <p>
+ * This class is not intended to be publicly instantiable, but is used for malformed JIDs during parsing automatically.
+ *
+ * @author Christian Schudt
+ * @see <a href="https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions-jid-malformed">RFC 6120, 8.3.3.8. jid-malformed</a>
+ */
+public final class MalformedJid extends AbstractJid {
+
+ private static final long serialVersionUID = -2896737611021417985L;
+
+ private final String localPart;
+
+ private final String domainPart;
+
+ private final String resourcePart;
+
+ private final Throwable cause;
+
+ static MalformedJid of(final String jid, final Throwable cause) {
+ // Do some basic parsing without any further checks or validation.
+ final StringBuilder sb = new StringBuilder(jid);
+ // 1. Remove any portion from the first '/' character to the end of the
+ // string (if there is a '/' character present).
+ final int indexOfResourceDelimiter = jid.indexOf('/');
+ final String resourcePart;
+ if (indexOfResourceDelimiter > -1) {
+ resourcePart = sb.substring(indexOfResourceDelimiter + 1);
+ sb.delete(indexOfResourceDelimiter, sb.length());
+ } else {
+ resourcePart = null;
+ }
+ // 2. Remove any portion from the beginning of the string to the first
+ // '@' character (if there is an '@' character present).
+ final int indexOfAt = jid.indexOf('@');
+ final String localPart;
+ if (indexOfAt > -1) {
+ localPart = sb.substring(0, indexOfAt);
+ sb.delete(0, indexOfAt + 1);
+ } else {
+ localPart = null;
+ }
+ return new MalformedJid(localPart, sb.toString(), resourcePart, cause);
+ }
+
+ private MalformedJid(final String localPart, final String domainPart, final String resourcePart, final Throwable cause) {
+ this.localPart = localPart;
+ this.domainPart = domainPart;
+ this.resourcePart = resourcePart;
+ this.cause = cause;
+ }
+
+ @Override
+ public final Jid asBareJid() {
+ return new MalformedJid(localPart, domainPart, null, cause);
+ }
+
+ @Override
+ public Jid withLocal(CharSequence local) {
+ return new MalformedJid(local.toString(), domainPart, resourcePart, cause);
+ }
+
+ @Override
+ public Jid withResource(CharSequence resource) {
+ return new MalformedJid(localPart, domainPart, resource.toString(), cause);
+ }
+
+ @Override
+ public Jid atSubdomain(CharSequence subdomain) {
+ if (subdomain == null) {
+ throw new NullPointerException();
+ }
+ return new MalformedJid(localPart, subdomain + "." + domainPart, resourcePart, cause);
+ }
+
+ @Override
+ public final String getLocal() {
+ return localPart;
+ }
+
+ @Override
+ public final String getEscapedLocal() {
+ return localPart;
+ }
+
+ @Override
+ public final String getDomain() {
+ return domainPart;
+ }
+
+ @Override
+ public final String getResource() {
+ return resourcePart;
+ }
+
+ /**
+ * Gets the cause why the JID is malformed.
+ *
+ * @return The cause.
+ */
+ public final Throwable getCause() {
+ return cause;
+ }
+}
diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/package-info.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/package-info.java
new file mode 100644
index 000000000..e12485d5f
--- /dev/null
+++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+/**
+ * Provides classes for the <a href="https://tools.ietf.org/html/rfc7622">XMPP Address Format</a> (JID).
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7622">Extensible Messaging and Presence Protocol (XMPP): Address Format</a>
+ */
+package rocks.xmpp.addr;
+
diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/DirectoryCache.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/DirectoryCache.java
new file mode 100644
index 000000000..9b7d66d04
--- /dev/null
+++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/DirectoryCache.java
@@ -0,0 +1,192 @@
+/*
+ * 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.util.cache;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * A simple directory based cache for caching of persistent items like avatars or entity capabilities.
+ *
+ * @author Christian Schudt
+ */
+public final class DirectoryCache implements Map<String, byte[]> {
+
+ private final Path cacheDirectory;
+
+ public DirectoryCache(Path cacheDirectory) {
+ this.cacheDirectory = cacheDirectory;
+ }
+
+ @Override
+ public final int size() {
+ try (final Stream<Path> files = cacheContent()) {
+ return (int) Math.min(files.count(), Integer.MAX_VALUE);
+ }
+ }
+
+ @Override
+ public final boolean isEmpty() {
+ try (final Stream<Path> files = cacheContent()) {
+ return files.findAny().map(file -> Boolean.FALSE).orElse(Boolean.TRUE);
+ }
+ }
+
+ @Override
+ public final boolean containsKey(Object key) {
+ return Files.exists(cacheDirectory.resolve(key.toString()));
+ }
+
+ @Override
+ public final boolean containsValue(Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final byte[] get(final Object key) {
+ return Optional.ofNullable(key).map(Object::toString).filter(((Predicate<String>) String::isEmpty).negate()).map(cacheDirectory::resolve).filter(Files::isReadable).map(file -> {
+ try {
+ return Files.readAllBytes(file);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }).orElse(null);
+ }
+
+ @Override
+ public final byte[] put(String key, byte[] value) {
+ // Make sure the directory exists.
+ byte[] data = get(key);
+ if (!Arrays.equals(data, value))
+ try {
+ if (Files.notExists(cacheDirectory)) {
+ Files.createDirectories(cacheDirectory);
+ }
+ Path file = cacheDirectory.resolve(key);
+ Files.write(file, value);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return data;
+ }
+
+ @Override
+ public final byte[] remove(Object key) {
+ byte[] data = get(key);
+ try {
+ Files.deleteIfExists(cacheDirectory.resolve(key.toString()));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return data;
+ }
+
+ @Override
+ public final void putAll(Map<? extends String, ? extends byte[]> m) {
+ m.forEach(this::put);
+ }
+
+ @Override
+ public final void clear() {
+ try {
+ Files.walkFileTree(cacheDirectory, new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ Files.deleteIfExists(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ // Don't delete the cache directory itself.
+ if (!Files.isSameFile(dir, cacheDirectory)) {
+ Files.deleteIfExists(dir);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public final Set<String> keySet() {
+ try (final Stream<Path> files = Files.list(cacheDirectory)) {
+ return Collections.unmodifiableSet(files.map(Path::getFileName).map(Path::toString).collect(Collectors.toSet()));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public final Collection<byte[]> values() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final Set<Entry<String, byte[]>> entrySet() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final void forEach(final BiConsumer<? super String, ? super byte[]> action) {
+ if (Files.exists(cacheDirectory))
+ try (final Stream<Path> files = cacheContent().filter(Files::isReadable)) {
+ files.forEach(file -> {
+ try {
+ action.accept(file.getFileName().toString(), Files.readAllBytes(file));
+ } catch (final IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+ }
+ }
+
+ @SuppressWarnings("StreamResourceLeak")
+ private final Stream<Path> cacheContent() {
+ try {
+ return Files.walk(cacheDirectory).filter(Files::isRegularFile);
+ } catch (final IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+}
diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/LruCache.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/LruCache.java
new file mode 100644
index 000000000..c2fbb0c3f
--- /dev/null
+++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/LruCache.java
@@ -0,0 +1,228 @@
+/*
+ * 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.util.cache;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+/**
+ * A simple concurrent implementation of a least-recently-used cache.
+ * <p>
+ * This cache is keeps a maximal number of items in memory and removes the least-recently-used item, when new items are added.
+ *
+ * @param <K> The key.
+ * @param <V> The value.
+ * @author Christian Schudt
+ * @see <a href="http://javadecodedquestions.blogspot.de/2013/02/java-cache-static-data-loading.html">http://javadecodedquestions.blogspot.de/2013/02/java-cache-static-data-loading.html</a>
+ * @see <a href="http://stackoverflow.com/a/22891780">http://stackoverflow.com/a/22891780</a>
+ */
+public final class LruCache<K, V> implements Map<K, V> {
+ private final int maxEntries;
+
+ private final Map<K, V> map;
+
+ final Queue<K> queue;
+
+ public LruCache(final int maxEntries) {
+ this.maxEntries = maxEntries;
+ this.map = new ConcurrentHashMap<>(maxEntries);
+ // Don't use a ConcurrentLinkedQueue here.
+ // There's a JDK bug, leading to OutOfMemoryError and high CPU usage:
+ // https://bugs.openjdk.java.net/browse/JDK-8054446
+ this.queue = new ConcurrentLinkedDeque<>();
+ }
+
+ @Override
+ public final int size() {
+ return map.size();
+ }
+
+ @Override
+ public final boolean isEmpty() {
+ return map.isEmpty();
+ }
+
+ @Override
+ public final boolean containsKey(final Object key) {
+ return map.containsKey(key);
+ }
+
+ @Override
+ public final boolean containsValue(final Object value) {
+ return map.containsValue(value);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final V get(final Object key) {
+ final V v = map.get(key);
+ if (v != null) {
+ // Remove the key from the queue and re-add it to the tail. It is now the most recently used key.
+ keyUsed((K) key);
+ }
+ return v;
+ }
+
+
+ @Override
+ public final V put(final K key, final V value) {
+ V v = map.put(key, value);
+ keyUsed(key);
+ limit();
+ return v;
+ }
+
+ @Override
+ public final V remove(final Object key) {
+ queue.remove(key);
+ return map.remove(key);
+ }
+
+
+ @Override
+ public final void putAll(final Map<? extends K, ? extends V> m) {
+ for (Map.Entry<? extends K, ? extends V> entry : m.entrySet()) {
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public final void clear() {
+ queue.clear();
+ map.clear();
+ }
+
+ @Override
+ public final Set<K> keySet() {
+ return map.keySet();
+ }
+
+ @Override
+ public final Collection<V> values() {
+ return map.values();
+ }
+
+ @Override
+ public final Set<Entry<K, V>> entrySet() {
+ return map.entrySet();
+ }
+
+
+ // Default methods
+
+ @Override
+ public final V putIfAbsent(final K key, final V value) {
+ final V v = map.putIfAbsent(key, value);
+ if (v == null) {
+ keyUsed(key);
+ }
+ limit();
+ return v;
+ }
+
+ @Override
+ public final boolean remove(final Object key, final Object value) {
+ final boolean removed = map.remove(key, value);
+ if (removed) {
+ queue.remove(key);
+ }
+ return removed;
+ }
+
+ @Override
+ public final boolean replace(final K key, final V oldValue, final V newValue) {
+ final boolean replaced = map.replace(key, oldValue, newValue);
+ if (replaced) {
+ keyUsed(key);
+ }
+ return replaced;
+ }
+
+ @Override
+ public final V replace(final K key, final V value) {
+ final V v = map.replace(key, value);
+ if (v != null) {
+ keyUsed(key);
+ }
+ return v;
+ }
+
+ @Override
+ public final V computeIfAbsent(final K key, final Function<? super K, ? extends V> mappingFunction) {
+ return map.computeIfAbsent(key, mappingFunction.<V>andThen(v -> {
+ keyUsed(key);
+ limit();
+ return v;
+ }));
+ }
+
+ @Override
+ public final V computeIfPresent(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+ return map.computeIfPresent(key, remappingFunction.<V>andThen(v -> {
+ keyUsed(key);
+ limit();
+ return v;
+ }));
+ }
+
+ @Override
+ public final V compute(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+ return map.compute(key, remappingFunction.<V>andThen(v -> {
+ keyUsed(key);
+ limit();
+ return v;
+ }));
+ }
+
+ @Override
+ public final V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
+ return map.merge(key, value, remappingFunction.<V>andThen(v -> {
+ keyUsed(key);
+ limit();
+ return v;
+ }));
+ }
+
+ private void limit() {
+ while (queue.size() > maxEntries) {
+ final K oldestKey = queue.poll();
+ if (oldestKey != null) {
+ map.remove(oldestKey);
+ }
+ }
+ }
+
+ private void keyUsed(final K key) {
+ // remove it from the queue and re-add it, to make it the most recently used key.
+ queue.remove(key);
+ queue.offer(key);
+ }
+} \ No newline at end of file
diff --git a/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/package-info.java b/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/package-info.java
new file mode 100644
index 000000000..c5e449d4c
--- /dev/null
+++ b/libs/xmpp-addr/src/main/java/rocks/xmpp/util/cache/package-info.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+/**
+ * Provides simple cache implementations.
+ */
+package rocks.xmpp.util.cache; \ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 51c45b1bc..333bfa6bc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,3 @@
include ':libs:android-transcoder'
+include ':libs:xmpp-addr'
rootProject.name = 'PixArtMessenger'
diff --git a/src/main/java/de/pixart/messenger/ui/util/MyLinkify.java b/src/main/java/de/pixart/messenger/ui/util/MyLinkify.java
index 2817c55c7..8f2aa4e61 100644
--- a/src/main/java/de/pixart/messenger/ui/util/MyLinkify.java
+++ b/src/main/java/de/pixart/messenger/ui/util/MyLinkify.java
@@ -40,6 +40,8 @@ import de.pixart.messenger.utils.GeoHelper;
import de.pixart.messenger.utils.Patterns;
import de.pixart.messenger.utils.XmppUri;
+import static java.lang.Character.isAlphabetic;
+
public class MyLinkify {
private static final Linkify.TransformFilter WEBURL_TRANSFORM_FILTER = (matcher, url) -> {
@@ -81,7 +83,7 @@ public class MyLinkify {
if (end < cs.length()) {
// Reject strings that were probably matched only because they contain a dot followed by
// by some known TLD (see also comment for WORD_BOUNDARY in Patterns.java)
- if (Character.isAlphabetic(cs.charAt(end - 1)) && Character.isAlphabetic(cs.charAt(end))) {
+ if (isAlphabetic(cs.charAt(end - 1)) && isAlphabetic(cs.charAt(end))) {
return false;
}
}
@@ -95,6 +97,23 @@ public class MyLinkify {
return uri.isJidValid();
};
+ private static boolean isAlphabetic(final int code) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ return Character.isAlphabetic(code);
+ }
+ switch (Character.getType(code)) {
+ case Character.UPPERCASE_LETTER:
+ case Character.LOWERCASE_LETTER:
+ case Character.TITLECASE_LETTER:
+ case Character.MODIFIER_LETTER:
+ case Character.OTHER_LETTER:
+ case Character.LETTER_NUMBER:
+ return true;
+ default:
+ return false;
+ }
+ }
+
public static void addLinks(Editable body, boolean includeGeo) {
Linkify.addLinks(body, Patterns.XMPP_PATTERN, "xmpp", XMPPURI_MATCH_FILTER, null);
Linkify.addLinks(body, Patterns.AUTOLINK_WEB_URL, "http", WEBURL_MATCH_FILTER, WEBURL_TRANSFORM_FILTER);