diff --git a/build.gradle b/build.gradle index fc088490d..2f30c289f 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ dependencies { implementation 'com.makeramen:roundedimageview:2.3.0' implementation "com.wefika:flowlayout:0.4.1" implementation 'net.ypresto.androidtranscoder:android-transcoder:0.2.0' - implementation 'rocks.xmpp:xmpp-addr:0.8.0' + implementation project(':libs:xmpp-addr') implementation 'org.osmdroid:osmdroid-android:6.0.1' implementation 'org.hsluv:hsluv:0.2' implementation 'org.conscrypt:conscrypt-android:1.3.0' @@ -73,7 +73,7 @@ android { compileSdkVersion 28 defaultConfig { - minSdkVersion 19 + minSdkVersion 16 targetSdkVersion 28 versionCode 307 versionName "2.3.9" 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. + *
+ *

The term "full JID" refers to an XMPP address of the form <localpart@domainpart/resourcepart> (for a particular authorized client or device associated with an account) or of the form <domainpart/resourcepart> (for a particular resource or script associated with a server).

+ *
+ * + * @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. + *
+ *

The term "bare JID" refers to an XMPP address of the form <localpart@domainpart> (for an account at a server) or of the form <domainpart> (for a server).

+ *
+ * + * @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 Extensible Messaging and Presence Protocol (XMPP): Address Format. + *

+ * This class is thread-safe and immutable. + * + * @author Christian Schudt + * @see RFC 7622 - Extensible Messaging and Presence Protocol (XMPP): Address Format + */ +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 ESCAPED_CACHE = new LruCache<>(5000); + + /** + * Caches the unescaped JIDs. + */ + private static final Map 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 + *

[ localpart "@" ] domainpart [ "/" resourcepart ]

+ * + * @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 XEP-0106: JID Escaping + */ + 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 XEP-0106: JID Escaping + */ + 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. + *
+ *

The term "bare JID" refers to an XMPP address of the form <localpart@domainpart> (for an account at a server) or of the form <domainpart> (for a server).

+ *
+ * + * @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. + *
+ *

3.3. Localpart

+ *

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., <localpart@domainpart>).

+ *
+ * + * @return The local part or null. + */ + @Override + public final String getLocal() { + return local; + } + + @Override + public final String getEscapedLocal() { + return escapedLocal; + } + + /** + * Gets the domain part. + *
+ *

3.2. Domainpart

+ *

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.

+ *
+ * + * @return The domain part. + */ + @Override + public final String getDomain() { + return domain; + } + + /** + * Gets the resource part. + *
+ *

3.4. Resourcepart

+ *

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 <localpart@domainpart> address or a + * mere <domainpart> 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., <localpart@domainpart/resourcepart>).

+ *
+ * + * @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 RFC 5895 + */ + 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 and ) 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 Extensible Messaging and Presence Protocol (XMPP): Address Format. + *

+ * A JID consists of three parts: + *

+ * [ localpart "@" ] domainpart [ "/" resourcepart ] + *

+ * 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 equals() and hashCode(), 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 XEP-0106: JID Escaping, i.e. + * ```java + * Jid.of("d'artagnan@musketeers.lit") + * ``` + * is escaped as d\\27artagnan@musketeers.lit. + *

+ * Implementations of this interface should be thread-safe and immutable. + * + * @author Christian Schudt + * @see RFC 7622 - Extensible Messaging and Presence Protocol (XMPP): Address Format + */ +@XmlJavaTypeAdapter(JidAdapter.class) +public interface Jid extends Comparable, Serializable, CharSequence { + + /** + * The maximal length of a full JID, which is 3071. + *

+ *

3.1. Fundamentals

+ *

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. + *

+ *
+ * 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 (jid\20escaping). + */ + String ESCAPING_FEATURE = "jid\\20escaping"; + + /** + * Returns a full JID with a domain and resource part, e.g. capulet.com/balcony + * + * @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. capulet.com + * + * @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. juliet@capulet.com + * + * @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. capulet.com/balcony + * + * @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 + *

[ localpart "@" ] domainpart [ "/" resourcepart ]

+ * 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 XEP-0106: JID Escaping + */ + 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 + *

[ localpart "@" ] domainpart [ "/" resourcepart ]

+ * 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 XEP-0106: JID Escaping + */ + static Jid ofEscaped(CharSequence jid) { + return FullJid.of(jid.toString(), true); + } + + /** + * Checks if the JID is a full JID. + *
+ *

The term "full JID" refers to an XMPP address of the form <localpart@domainpart/resourcepart> (for a particular authorized client or device associated with an account) or of the form <domainpart/resourcepart> (for a particular resource or script associated with a server).

+ *
+ * + * @return True, if the JID is a full JID; otherwise false. + */ + boolean isFullJid(); + + /** + * Checks if the JID is a bare JID. + *
+ *

The term "bare JID" refers to an XMPP address of the form <localpart@domainpart> (for an account at a server) or of the form <domainpart> (for a server).

+ *
+ * + * @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. + *
+ *

The term "bare JID" refers to an XMPP address of the form <localpart@domainpart> (for an account at a server) or of the form <domainpart> (for a server).

+ *
+ * + * @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. + *
+ *

3.3. Localpart

+ *

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., <localpart@domainpart>).

+ *
+ * + * @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. + *
+ *

3.2. Domainpart

+ *

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.

+ *
+ * + * @return The domain part. + */ + String getDomain(); + + /** + * Gets the resource part. + *
+ *

3.4. Resourcepart

+ *

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 <localpart@domainpart> address or a + * mere <domainpart> 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., <localpart@domainpart/resourcepart>).

+ *
+ * + * @return The resource part or null. + */ + String getResource(); + + /** + * Returns the JID in escaped form as described in XEP-0106: JID Escaping. + * + * @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 { + + @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 jid-malformed error. + *

+ * This class is not intended to be publicly instantiable, but is used for malformed JIDs during parsing automatically. + * + * @author Christian Schudt + * @see RFC 6120, 8.3.3.8. jid-malformed + */ +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 XMPP Address Format (JID). + * + * @see Extensible Messaging and Presence Protocol (XMPP): Address Format + */ +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 { + + private final Path cacheDirectory; + + public DirectoryCache(Path cacheDirectory) { + this.cacheDirectory = cacheDirectory; + } + + @Override + public final int size() { + try (final Stream files = cacheContent()) { + return (int) Math.min(files.count(), Integer.MAX_VALUE); + } + } + + @Override + public final boolean isEmpty() { + try (final Stream 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::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 m) { + m.forEach(this::put); + } + + @Override + public final void clear() { + try { + Files.walkFileTree(cacheDirectory, new SimpleFileVisitor() { + @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 keySet() { + try (final Stream 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 values() { + throw new UnsupportedOperationException(); + } + + @Override + public final Set> entrySet() { + throw new UnsupportedOperationException(); + } + + @Override + public final void forEach(final BiConsumer action) { + if (Files.exists(cacheDirectory)) + try (final Stream 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 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. + *

+ * This cache is keeps a maximal number of items in memory and removes the least-recently-used item, when new items are added. + * + * @param The key. + * @param The value. + * @author Christian Schudt + * @see http://javadecodedquestions.blogspot.de/2013/02/java-cache-static-data-loading.html + * @see http://stackoverflow.com/a/22891780 + */ +public final class LruCache implements Map { + private final int maxEntries; + + private final Map map; + + final Queue 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 m) { + for (Map.Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public final void clear() { + queue.clear(); + map.clear(); + } + + @Override + public final Set keySet() { + return map.keySet(); + } + + @Override + public final Collection values() { + return map.values(); + } + + @Override + public final Set> 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 mappingFunction) { + return map.computeIfAbsent(key, mappingFunction.andThen(v -> { + keyUsed(key); + limit(); + return v; + })); + } + + @Override + public final V computeIfPresent(final K key, final BiFunction remappingFunction) { + return map.computeIfPresent(key, remappingFunction.andThen(v -> { + keyUsed(key); + limit(); + return v; + })); + } + + @Override + public final V compute(final K key, final BiFunction remappingFunction) { + return map.compute(key, remappingFunction.andThen(v -> { + keyUsed(key); + limit(); + return v; + })); + } + + @Override + public final V merge(K key, V value, BiFunction remappingFunction) { + return map.merge(key, value, remappingFunction.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 4193570fa..746b2c7e3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ +include ':libs:xmpp-addr' rootProject.name = 'Conversations' diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java index d350e51f9..d944f2d08 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java @@ -29,6 +29,7 @@ package eu.siacs.conversations.ui.util; +import android.os.Build; import android.text.Editable; import android.text.util.Linkify; @@ -80,7 +81,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; } } @@ -93,6 +94,24 @@ 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); diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index 680299a28..96ce63c90 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -65,7 +65,9 @@ public class Resolver { final Field useHardcodedDnsServers = DNSClient.class.getDeclaredField("useHardcodedDnsServers"); useHardcodedDnsServers.setAccessible(true); useHardcodedDnsServers.setBoolean(dnsClient, false); - } catch (NoSuchFieldException | IllegalAccessException e) { + } catch (NoSuchFieldException e) { + Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e); + } catch (IllegalAccessException e) { Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e); } } diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml index 7ab0894ce..1258c4db0 100644 --- a/src/main/res/layout/activity_muc_details.xml +++ b/src/main/res/layout/activity_muc_details.xml @@ -48,14 +48,17 @@ android:layout_height="@dimen/avatar_on_details_screen_size" android:layout_alignParentStart="true" app:riv_corner_radius="2dp" - android:layout_marginEnd="@dimen/avatar_item_distance"/> + android:layout_marginEnd="@dimen/avatar_item_distance" + android:layout_alignParentLeft="true" + android:layout_marginRight="@dimen/avatar_item_distance" /> + android:orientation="vertical" + android:layout_toRightOf="@+id/your_photo"> + android:orientation="vertical" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/edit_muc_name_button"> + android:visibility="gone" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/edit_muc_name_button"> + android:src="?attr/icon_edit_body" + android:layout_alignParentRight="true" /> @@ -151,7 +159,8 @@ android:layout_toStartOf="@+id/change_conference_button" android:text="@string/private_conference" android:textAppearance="@style/TextAppearance.Conversations.Body1" - /> + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/change_conference_button" /> + android:src="?attr/icon_settings" + android:layout_alignParentRight="true" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" + android:paddingLeft="4dp" /> @@ -302,7 +313,8 @@ android:alpha="?attr/icon_alpha" android:background="?attr/selectableItemBackgroundBorderless" android:padding="@dimen/image_button_padding" - android:src="?attr/icon_edit_body"/> + android:src="?attr/icon_edit_body" + android:layout_alignParentRight="true" /> + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/notification_status_button" /> + android:src="?attr/icon_notifications" + android:layout_alignParentRight="true" /> + android:src="?attr/icon_gps_fixed" + android:layout_alignParentRight="true" /> + app:tint="@color/white" + android:layout_alignParentRight="true" /> \ No newline at end of file diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index 5a9048db7..9bd97c045 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -21,7 +21,8 @@ android:listSelector="@android:color/transparent" android:stackFromBottom="true" android:transcriptMode="normal" - tools:listitem="@layout/message_sent"> + tools:listitem="@layout/message_sent" + android:layout_alignParentLeft="true"> + app:useCompatPadding="true" + android:layout_alignParentRight="true" /> + app:backgroundColor="?attr/unread_count" + android:layout_alignRight="@+id/scroll_to_bottom_button" + tools:ignore="RtlCompat" + android:layout_marginRight="8dp" /> + android:background="?attr/color_background_primary" + android:layout_alignParentLeft="true"> + android:requiresFadingEdge="horizontal" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/textSendButton"> @@ -89,7 +97,9 @@ android:paddingBottom="12dp" android:paddingLeft="8dp" android:paddingRight="8dp" - android:paddingTop="12dp"> + android:paddingTop="12dp" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/textSendButton"> @@ -102,7 +112,8 @@ android:layout_centerVertical="true" android:background="?attr/color_background_primary" android:contentDescription="@string/send_message" - android:src="?attr/ic_send_text_offline"/> + android:src="?attr/ic_send_text_offline" + android:layout_alignParentRight="true" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1.OnDark" + android:layout_alignParentLeft="true" + android:paddingLeft="24dp" + android:layout_toLeftOf="@+id/snackbar_action" /> + android:textStyle="bold" + android:layout_alignParentRight="true" /> diff --git a/src/main/res/layout/media_preview.xml b/src/main/res/layout/media_preview.xml index 07da56f4e..2262467aa 100644 --- a/src/main/res/layout/media_preview.xml +++ b/src/main/res/layout/media_preview.xml @@ -19,6 +19,7 @@ android:layout_alignParentTop="true" android:alpha="?attr/delete_icon_alpha" android:background="?attr/selectableItemBackgroundBorderless" - android:src="?attr/icon_cancel"/> + android:src="?attr/icon_cancel" + android:layout_alignParentRight="true" /> diff --git a/src/main/res/layout/message_sent.xml b/src/main/res/layout/message_sent.xml index d6a387c62..29ac6fc65 100644 --- a/src/main/res/layout/message_sent.xml +++ b/src/main/res/layout/message_sent.xml @@ -17,7 +17,8 @@ android:layout_alignParentEnd="true" android:layout_alignParentBottom="true" android:layout_width="wrap_content" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:layout_alignParentRight="true">