diff options
author | Christian Schneppe <christian@pix-art.de> | 2018-12-14 20:48:25 +0100 |
---|---|---|
committer | Christian Schneppe <christian@pix-art.de> | 2018-12-14 21:13:31 +0100 |
commit | fe3f14d9b13f2cd97c24bc241dddaa2c2bca755f (patch) | |
tree | afdb45362c7bee8da2632be0e98424386d02544a /libs | |
parent | 7967a35cd3fa9983a559c937a7ab24ef815d87c6 (diff) |
Checkout `xmpp-addr` library 0.8.0 (fa47cac8) locally
xmpp-addr: Backfill missing class method for Java 1.7
This backfills missing class methods for `java.nio.charset.StandardCharsets`
and `java.util.Objects` for compatibility with platforms which do not support
these (mainly Android SDK versions <= 18).
Change `minSdkVersion` to 18, backfill missing methods
This reduces the minimum SDK version to 18 (Android 4.3), which notably is
the last supported version for the BlackBerry OS 10.3 Android compatibility
layer.
Drop support for Android 4.1 and 4.2
Diffstat (limited to 'libs')
11 files changed, 1669 insertions, 0 deletions
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 <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).</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 <localpart@domainpart> (for an account at a server) or of the form <domainpart> (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 <localpart@domainpart> (for an account at a server) or of the form <domainpart> (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., <localpart@domainpart>).</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 <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>).</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 <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).</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 <localpart@domainpart> (for an account at a server) or of the form <domainpart> (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 <localpart@domainpart> (for an account at a server) or of the form <domainpart> (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., <localpart@domainpart>).</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 <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>).</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 |