Merge branch 'api-level-18' of https://github.com/deuill/Conversations into deuill-api-level-18

This commit is contained in:
Daniel Gultsch 2018-12-10 13:13:16 +01:00
commit c071a53595
21 changed files with 1752 additions and 28 deletions

View File

@ -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"

1
libs/xmpp-addr/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,14 @@
apply plugin: 'java-library'
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
implementation 'rocks.xmpp:precis:1.0.0'
}
sourceCompatibility = "8"
targetCompatibility = "8"

View File

@ -0,0 +1,179 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2017 Christian Schudt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package rocks.xmpp.addr;
import java.text.Collator;
import java.util.Arrays;
/**
* Abstract Jid implementation for both full and bare JIDs.
*
* @author Christian Schudt
*/
abstract class AbstractJid implements Jid {
/**
* Checks if the JID is a full JID.
* <blockquote>
* <p>The term "full JID" refers to an XMPP address of the form &lt;localpart@domainpart/resourcepart&gt; (for a particular authorized client or device associated with an account) or of the form &lt;domainpart/resourcepart&gt; (for a particular resource or script associated with a server).</p>
* </blockquote>
*
* @return True, if the JID is a full JID; otherwise false.
*/
@Override
public final boolean isFullJid() {
return getResource() != null;
}
/**
* Checks if the JID is a bare JID.
* <blockquote>
* <p>The term "bare JID" refers to an XMPP address of the form &lt;localpart@domainpart&gt; (for an account at a server) or of the form &lt;domainpart&gt; (for a server).</p>
* </blockquote>
*
* @return True, if the JID is a bare JID; otherwise false.
*/
@Override
public final boolean isBareJid() {
return getResource() == null;
}
@Override
public final boolean isDomainJid() {
return getLocal() == null;
}
@Override
public final boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Jid)) {
return false;
}
Jid other = (Jid) o;
return (getLocal() == other.getLocal() || getLocal() != null && getLocal().equals(other.getLocal()))
&& (getDomain() == other.getDomain() || getDomain() != null && getDomain().equals(other.getDomain()))
&& (getResource() == other.getResource() || getResource() != null && getResource().equals(other.getResource()));
}
@Override
public final int hashCode() {
return Arrays.hashCode(new String[]{getLocal(), getDomain(), getResource()});
}
/**
* Compares this JID with another JID. First domain parts are compared. If these are equal, local parts are compared
* and if these are equal, too, resource parts are compared.
*
* @param o The other JID.
* @return The comparison result.
*/
@Override
public final int compareTo(Jid o) {
if (this == o) {
return 0;
}
if (o != null) {
final Collator collator = Collator.getInstance();
int result;
// First compare domain parts.
if (getDomain() != null) {
result = o.getDomain() != null ? collator.compare(getDomain(), o.getDomain()) : -1;
} else {
result = o.getDomain() != null ? 1 : 0;
}
// If the domains are equal, compare local parts.
if (result == 0) {
if (getLocal() != null) {
// If this local part is not null, but the other is null, move this down (1).
result = o.getLocal() != null ? collator.compare(getLocal(), o.getLocal()) : 1;
} else {
// If this local part is null, but the other is not, move this up (-1).
result = o.getLocal() != null ? -1 : 0;
}
}
// If the local parts are equal, compare resource parts.
if (result == 0) {
if (getResource() != null) {
// If this resource part is not null, but the other is null, move this down (1).
return o.getResource() != null ? collator.compare(getResource(), o.getResource()) : 1;
} else {
// If this resource part is null, but the other is not, move this up (-1).
return o.getResource() != null ? -1 : 0;
}
}
return result;
} else {
return -1;
}
}
@Override
public final int length() {
return toString().length();
}
@Override
public final char charAt(int index) {
return toString().charAt(index);
}
@Override
public final CharSequence subSequence(int start, int end) {
return toString().subSequence(start, end);
}
/**
* Returns the JID in its string representation, i.e. [ localpart "@" ] domainpart [ "/" resourcepart ].
*
* @return The JID.
* @see #toEscapedString()
*/
@Override
public final String toString() {
return toString(getLocal(), getDomain(), getResource());
}
@Override
public final String toEscapedString() {
return toString(getEscapedLocal(), getDomain(), getResource());
}
static String toString(String local, String domain, String resource) {
StringBuilder sb = new StringBuilder();
if (local != null) {
sb.append(local).append('@');
}
sb.append(domain);
if (resource != null) {
sb.append('/').append(resource);
}
return sb.toString();
}
}

View File

@ -0,0 +1,498 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2017 Christian Schudt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package rocks.xmpp.addr;
import rocks.xmpp.precis.PrecisProfile;
import rocks.xmpp.precis.PrecisProfiles;
import rocks.xmpp.util.cache.LruCache;
import java.net.IDN;
import java.nio.charset.Charset;
import java.text.Normalizer;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The implementation of the JID as described in <a href="https://tools.ietf.org/html/rfc7622">Extensible Messaging and Presence Protocol (XMPP): Address Format</a>.
* <p>
* This class is thread-safe and immutable.
*
* @author Christian Schudt
* @see <a href="https://tools.ietf.org/html/rfc7622">RFC 7622 - Extensible Messaging and Presence Protocol (XMPP): Address Format</a>
*/
final class FullJid extends AbstractJid {
/**
* Escapes all disallowed characters and also backslash, when followed by a defined hex code for escaping. See 4. Business Rules.
*/
private static final Pattern ESCAPE_PATTERN = Pattern.compile("[ \"&'/:<>@]|\\\\(?=20|22|26|27|2f|3a|3c|3e|40|5c)");
private static final Pattern UNESCAPE_PATTERN = Pattern.compile("\\\\(20|22|26|27|2f|3a|3c|3e|40|5c)");
private static final Pattern JID = Pattern.compile("^((.*?)@)?([^/@]+)(/(.*))?$");
private static final IDNProfile IDN_PROFILE = new IDNProfile();
/**
* Whenever dots are used as label separators, the following characters MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61 (halfwidth ideographic full stop).
*/
private static final String DOTS = "[.\u3002\uFF0E\uFF61]";
/**
* Label separators for domain labels, which should be mapped to "." (dot): IDEOGRAPHIC FULL STOP character (U+3002)
*/
private static final Pattern LABEL_SEPARATOR = Pattern.compile(DOTS);
private static final Pattern LABEL_SEPARATOR_FINAL = Pattern.compile(DOTS + "$");
/**
* Caches the escaped JIDs.
*/
private static final Map<CharSequence, Jid> ESCAPED_CACHE = new LruCache<>(5000);
/**
* Caches the unescaped JIDs.
*/
private static final Map<CharSequence, Jid> UNESCAPED_CACHE = new LruCache<>(5000);
private static final long serialVersionUID = -3824234106101731424L;
private final String escapedLocal;
private final String local;
private final String domain;
private final String resource;
private final Jid bareJid;
/**
* Creates a full JID with local, domain and resource part.
*
* @param local The local part.
* @param domain The domain part.
* @param resource The resource part.
*/
FullJid(CharSequence local, CharSequence domain, CharSequence resource) {
this(local, domain, resource, false, null);
}
private FullJid(final CharSequence local, final CharSequence domain, final CharSequence resource, final boolean doUnescape, Jid bareJid) {
final String enforcedLocalPart;
final String enforcedDomainPart;
final String enforcedResource;
final String unescapedLocalPart;
if (domain == null) {
throw new NullPointerException();
}
if (doUnescape) {
unescapedLocalPart = unescape(local);
} else {
unescapedLocalPart = local != null ? local.toString() : null;
}
// Escape the local part, so that disallowed characters like the space characters pass the UsernameCaseMapped profile.
final String escapedLocalPart = escape(unescapedLocalPart);
// If the domainpart includes a final character considered to be a label
// separator (dot) by [RFC1034], this character MUST be stripped from
// the domainpart before the JID of which it is a part is used for the
// purpose of routing an XML stanza, comparing against another JID, or
// constructing an XMPP URI or IRI [RFC5122]. In particular, such a
// character MUST be stripped before any other canonicalization steps
// are taken.
// Also validate, that the domain name can be converted to ASCII, i.e. validate the domain name (e.g. must not start with "_").
final String strDomain = IDN.toASCII(LABEL_SEPARATOR_FINAL.matcher(domain).replaceAll(""), IDN.USE_STD3_ASCII_RULES);
enforcedLocalPart = escapedLocalPart != null ? PrecisProfiles.USERNAME_CASE_MAPPED.enforce(escapedLocalPart) : null;
enforcedResource = resource != null ? PrecisProfiles.OPAQUE_STRING.enforce(resource) : null;
// See https://tools.ietf.org/html/rfc5895#section-2
enforcedDomainPart = IDN_PROFILE.enforce(strDomain);
validateLength(enforcedLocalPart, "local");
validateLength(enforcedResource, "resource");
validateDomain(strDomain);
this.local = unescape(enforcedLocalPart);
this.escapedLocal = enforcedLocalPart;
this.domain = enforcedDomainPart;
this.resource = enforcedResource;
if (bareJid != null) {
this.bareJid = bareJid;
} else {
this.bareJid = isBareJid() ? this : new AbstractJid() {
@Override
public Jid asBareJid() {
return this;
}
@Override
public Jid withLocal(CharSequence local) {
if (local == this.getLocal() || local != null && local.equals(this.getLocal())) {
return this;
}
return new FullJid(local, getDomain(), getResource(), false, null);
}
@Override
public Jid withResource(CharSequence resource) {
if (resource == this.getResource() || resource != null && resource.equals(this.getResource())) {
return this;
}
return new FullJid(getLocal(), getDomain(), resource, false, asBareJid());
}
@Override
public Jid atSubdomain(CharSequence subdomain) {
if (subdomain == null) {
throw new NullPointerException();
}
return new FullJid(getLocal(), subdomain + "." + getDomain(), getResource(), false, null);
}
@Override
public String getLocal() {
return FullJid.this.getLocal();
}
@Override
public String getEscapedLocal() {
return FullJid.this.getEscapedLocal();
}
@Override
public String getDomain() {
return FullJid.this.getDomain();
}
@Override
public String getResource() {
return null;
}
};
}
}
/**
* Creates a JID from a string. The format must be
* <blockquote><p>[ localpart "@" ] domainpart [ "/" resourcepart ]</p></blockquote>
*
* @param jid The JID.
* @param doUnescape If the jid parameter will be unescaped.
* @return The JID.
* @throws NullPointerException If the jid is null.
* @throws IllegalArgumentException If the jid could not be parsed or is not valid.
* @see <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>
*/
static Jid of(String jid, final boolean doUnescape) {
if (jid == null) {
throw new NullPointerException("jid must not be null.");
}
jid = jid.trim();
if (jid.isEmpty()) {
throw new IllegalArgumentException("jid must not be empty.");
}
Jid result;
if (doUnescape) {
result = UNESCAPED_CACHE.get(jid);
} else {
result = ESCAPED_CACHE.get(jid);
}
if (result != null) {
return result;
}
Matcher matcher = JID.matcher(jid);
if (matcher.matches()) {
Jid jidValue = new FullJid(matcher.group(2), matcher.group(3), matcher.group(5), doUnescape, null);
if (doUnescape) {
UNESCAPED_CACHE.put(jid, jidValue);
} else {
ESCAPED_CACHE.put(jid, jidValue);
}
return jidValue;
} else {
throw new IllegalArgumentException("Could not parse JID: " + jid);
}
}
/**
* Escapes a local part. The characters {@code "&'/:<>@} (+ whitespace) are replaced with their respective escape characters.
*
* @param localPart The local part.
* @return The escaped local part or null.
* @see <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>
*/
private static String escape(final CharSequence localPart) {
if (localPart != null) {
final Matcher matcher = ESCAPE_PATTERN.matcher(localPart);
final StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, "\\\\" + Integer.toHexString(matcher.group().charAt(0)));
}
matcher.appendTail(sb);
return sb.toString();
}
return null;
}
private static String unescape(final CharSequence localPart) {
if (localPart != null) {
final Matcher matcher = UNESCAPE_PATTERN.matcher(localPart);
final StringBuffer sb = new StringBuffer();
while (matcher.find()) {
final char c = (char) Integer.parseInt(matcher.group(1), 16);
if (c == '\\') {
matcher.appendReplacement(sb, "\\\\");
} else {
matcher.appendReplacement(sb, String.valueOf(c));
}
}
matcher.appendTail(sb);
return sb.toString();
}
return null;
}
private static void validateDomain(String domain) {
if (domain == null) {
throw new NullPointerException("domain must not be null.");
}
if (domain.contains("@")) {
// Prevent misuse of API.
throw new IllegalArgumentException("domain must not contain a '@' sign");
}
validateLength(domain, "domain");
}
/**
* Validates that the length of a local, domain or resource part is not longer than 1023 characters.
*
* @param value The value.
* @param part The part, only used to produce an exception message.
*/
private static void validateLength(CharSequence value, CharSequence part) {
if (value != null) {
if (value.length() == 0) {
throw new IllegalArgumentException(part + " must not be empty.");
}
if (value.toString().getBytes(Charset.forName("UTF-8")).length > 1023) {
throw new IllegalArgumentException(part + " must not be greater than 1023 bytes.");
}
}
}
/**
* Converts this JID into a bare JID, i.e. removes the resource part.
* <blockquote>
* <p>The term "bare JID" refers to an XMPP address of the form &lt;localpart@domainpart&gt; (for an account at a server) or of the form &lt;domainpart&gt; (for a server).</p>
* </blockquote>
*
* @return The bare JID.
* @see #withResource(CharSequence)
*/
@Override
public final Jid asBareJid() {
return bareJid;
}
/**
* Gets the local part of the JID, also known as the name or node.
* <blockquote>
* <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.3">3.3. Localpart</a></cite></p>
* <p>The localpart of a JID is an optional identifier placed before the
* domainpart and separated from the latter by the '@' character.
* Typically, a localpart uniquely identifies the entity requesting and
* using network access provided by a server (i.e., a local account),
* although it can also represent other kinds of entities (e.g., a
* chatroom associated with a multi-user chat service [XEP-0045]). The
* entity represented by an XMPP localpart is addressed within the
* context of a specific domain (i.e., &lt;localpart@domainpart&gt;).</p>
* </blockquote>
*
* @return The local part or null.
*/
@Override
public final String getLocal() {
return local;
}
@Override
public final String getEscapedLocal() {
return escapedLocal;
}
/**
* Gets the domain part.
* <blockquote>
* <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.2">3.2. Domainpart</a></cite></p>
* <p>The domainpart is the primary identifier and is the only REQUIRED
* element of a JID (a mere domainpart is a valid JID). Typically,
* a domainpart identifies the "home" server to which clients connect
* for XML routing and data management functionality.</p>
* </blockquote>
*
* @return The domain part.
*/
@Override
public final String getDomain() {
return domain;
}
/**
* Gets the resource part.
* <blockquote>
* <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.4">3.4. Resourcepart</a></cite></p>
* <p>The resourcepart of a JID is an optional identifier placed after the
* domainpart and separated from the latter by the '/' character. A
* resourcepart can modify either a &lt;localpart@domainpart&gt; address or a
* mere &lt;domainpart&gt; address. Typically, a resourcepart uniquely
* identifies a specific connection (e.g., a device or location) or
* object (e.g., an occupant in a multi-user chatroom [XEP-0045])
* belonging to the entity associated with an XMPP localpart at a domain
* (i.e., &lt;localpart@domainpart/resourcepart&gt;).</p>
* </blockquote>
*
* @return The resource part or null.
*/
@Override
public final String getResource() {
return resource;
}
/**
* Creates a new JID with a new local part and the same domain and resource part of the current JID.
*
* @param local The local part.
* @return The JID with a new local part.
* @throws IllegalArgumentException If the local is not a valid local part.
* @see #withResource(CharSequence)
*/
@Override
public final Jid withLocal(CharSequence local) {
if (local == this.getLocal() || local != null && local.equals(this.getLocal())) {
return this;
}
return new FullJid(local, getDomain(), getResource(), false, null);
}
/**
* Creates a new full JID with a resource and the same local and domain part of the current JID.
*
* @param resource The resource.
* @return The full JID with a resource.
* @throws IllegalArgumentException If the resource is not a valid resource part.
* @see #asBareJid()
* @see #withLocal(CharSequence)
*/
@Override
public final Jid withResource(CharSequence resource) {
if (resource == this.getResource() || resource != null && resource.equals(this.getResource())) {
return this;
}
return new FullJid(getLocal(), getDomain(), resource, false, asBareJid());
}
/**
* Creates a new JID at a subdomain and at the same domain as this JID.
*
* @param subdomain The subdomain.
* @return The JID at a subdomain.
* @throws NullPointerException If subdomain is null.
* @throws IllegalArgumentException If subdomain is not a valid subdomain name.
*/
@Override
public final Jid atSubdomain(CharSequence subdomain) {
if (subdomain != null) {
throw new NullPointerException();
}
return new FullJid(getLocal(), subdomain + "." + getDomain(), getResource(), false, null);
}
/**
* A profile for applying the rules for IDN as in RFC 5895. Although IDN doesn't use Precis, it's still very similar so that we can use the base class.
*
* @see <a href="https://tools.ietf.org/html/rfc5895#section-2">RFC 5895</a>
*/
private static final class IDNProfile extends PrecisProfile {
private IDNProfile() {
super(false);
}
@Override
public String prepare(CharSequence input) {
return IDN.toUnicode(input.toString(), IDN.USE_STD3_ASCII_RULES);
}
@Override
public String enforce(CharSequence input) {
// 4. Map IDEOGRAPHIC FULL STOP character (U+3002) to dot.
return applyAdditionalMappingRule(
// 3. All characters are mapped using Unicode Normalization Form C (NFC).
applyNormalizationRule(
// 2. Fullwidth and halfwidth characters (those defined with
// Decomposition Types <wide> and <narrow>) are mapped to their
// decomposition mappings
applyWidthMappingRule(
// 1. Uppercase characters are mapped to their lowercase equivalents
applyCaseMappingRule(prepare(input))))).toString();
}
@Override
protected CharSequence applyWidthMappingRule(CharSequence charSequence) {
return widthMap(charSequence);
}
@Override
protected CharSequence applyAdditionalMappingRule(CharSequence charSequence) {
return LABEL_SEPARATOR.matcher(charSequence).replaceAll(".");
}
@Override
protected CharSequence applyCaseMappingRule(CharSequence charSequence) {
return charSequence.toString().toLowerCase();
}
@Override
protected CharSequence applyNormalizationRule(CharSequence charSequence) {
return Normalizer.normalize(charSequence, Normalizer.Form.NFC);
}
@Override
protected CharSequence applyDirectionalityRule(CharSequence charSequence) {
return charSequence;
}
}
}

View File

@ -0,0 +1,314 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2017 Christian Schudt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package rocks.xmpp.addr;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.io.Serializable;
/**
* Represents the JID as described in <a href="https://tools.ietf.org/html/rfc7622">Extensible Messaging and Presence Protocol (XMPP): Address Format</a>.
* <p>
* A JID consists of three parts:
* <p>
* [ localpart "@" ] domainpart [ "/" resourcepart ]
* </p>
* The easiest way to create a JID is to use the {@link #of(CharSequence)} method:
* ```java
* Jid jid = Jid.of("juliet@capulet.lit/balcony");
* ```
* You can then get the parts from it via the respective methods:
* ```java
* String local = jid.getLocal(); // juliet
* String domain = jid.getDomain(); // capulet.lit
* String resource = jid.getResource(); // balcony
* ```
* Implementations of this interface should override <code>equals()</code> and <code>hashCode()</code>, so that different instances with the same value are equal:
* ```java
* Jid.of("romeo@capulet.lit/balcony").equals(Jid.of("romeo@capulet.lit/balcony")); // true
* ```
* The default implementation of this class also supports <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>, i.e.
* ```java
* Jid.of("d'artagnan@musketeers.lit")
* ```
* is escaped as <code>d\\27artagnan@musketeers.lit</code>.
* <p>
* Implementations of this interface should be thread-safe and immutable.
*
* @author Christian Schudt
* @see <a href="https://tools.ietf.org/html/rfc7622">RFC 7622 - Extensible Messaging and Presence Protocol (XMPP): Address Format</a>
*/
@XmlJavaTypeAdapter(JidAdapter.class)
public interface Jid extends Comparable<Jid>, Serializable, CharSequence {
/**
* The maximal length of a full JID, which is 3071.
* <blockquote>
* <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.1">3.1. Fundamentals</a></cite></p>
* <p>Each allowable portion of a JID (localpart, domainpart, and
* resourcepart) is 1 to 1023 octets in length, resulting in a maximum
* total size (including the '@' and '/' separators) of 3071 octets.
* </p>
* </blockquote>
* Note that the length is based on bytes, not characters.
*
* @see #MAX_BARE_JID_LENGTH
*/
int MAX_FULL_JID_LENGTH = 3071;
/**
* The maximal length of a bare JID, which is 2047 (1023 + 1 + 1023).
* Note that the length is based on bytes, not characters.
*
* @see #MAX_FULL_JID_LENGTH
*/
int MAX_BARE_JID_LENGTH = 2047;
/**
* The service discovery feature used for determining support of JID escaping (<code>jid\20escaping</code>).
*/
String ESCAPING_FEATURE = "jid\\20escaping";
/**
* Returns a full JID with a domain and resource part, e.g. <code>capulet.com/balcony</code>
*
* @param local The local part.
* @param domain The domain.
* @param resource The resource part.
* @return The JID.
* @throws NullPointerException If the domain is null.
* @throws IllegalArgumentException If the domain, local or resource part are not valid.
*/
static Jid of(CharSequence local, CharSequence domain, CharSequence resource) {
return new FullJid(local, domain, resource);
}
/**
* Creates a bare JID with only the domain part, e.g. <code>capulet.com</code>
*
* @param domain The domain.
* @return The JID.
* @throws NullPointerException If the domain is null.
* @throws IllegalArgumentException If the domain or local part are not valid.
*/
static Jid ofDomain(CharSequence domain) {
return new FullJid(null, domain, null);
}
/**
* Creates a bare JID with a local and domain part, e.g. <code>juliet@capulet.com</code>
*
* @param local The local part.
* @param domain The domain.
* @return The JID.
* @throws NullPointerException If the domain is null.
* @throws IllegalArgumentException If the domain or local part are not valid.
*/
static Jid ofLocalAndDomain(CharSequence local, CharSequence domain) {
return new FullJid(local, domain, null);
}
/**
* Creates a full JID with a domain and resource part, e.g. <code>capulet.com/balcony</code>
*
* @param domain The domain.
* @param resource The resource part.
* @return The JID.
* @throws NullPointerException If the domain is null.
* @throws IllegalArgumentException If the domain or resource are not valid.
*/
static Jid ofDomainAndResource(CharSequence domain, CharSequence resource) {
return new FullJid(null, domain, resource);
}
/**
* Creates a JID from an unescaped string. The format must be
* <blockquote><p>[ localpart "@" ] domainpart [ "/" resourcepart ]</p></blockquote>
* The input string will be escaped.
*
* @param jid The JID.
* @return The JID.
* @throws NullPointerException If the jid is null.
* @throws IllegalArgumentException If the jid could not be parsed or is not valid.
* @see <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>
*/
static Jid of(CharSequence jid) {
if (jid instanceof Jid) {
return (Jid) jid;
}
return FullJid.of(jid.toString(), false);
}
/**
* Creates a JID from a escaped JID string. The format must be
* <blockquote><p>[ localpart "@" ] domainpart [ "/" resourcepart ]</p></blockquote>
* This method should be used, when parsing JIDs from the XMPP stream.
*
* @param jid The JID.
* @return The JID.
* @throws NullPointerException If the jid is null.
* @throws IllegalArgumentException If the jid could not be parsed or is not valid.
* @see <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>
*/
static Jid ofEscaped(CharSequence jid) {
return FullJid.of(jid.toString(), true);
}
/**
* Checks if the JID is a full JID.
* <blockquote>
* <p>The term "full JID" refers to an XMPP address of the form &lt;localpart@domainpart/resourcepart&gt; (for a particular authorized client or device associated with an account) or of the form &lt;domainpart/resourcepart&gt; (for a particular resource or script associated with a server).</p>
* </blockquote>
*
* @return True, if the JID is a full JID; otherwise false.
*/
boolean isFullJid();
/**
* Checks if the JID is a bare JID.
* <blockquote>
* <p>The term "bare JID" refers to an XMPP address of the form &lt;localpart@domainpart&gt; (for an account at a server) or of the form &lt;domainpart&gt; (for a server).</p>
* </blockquote>
*
* @return True, if the JID is a bare JID; otherwise false.
*/
boolean isBareJid();
/**
* Checks if the JID is a domain JID, i.e. if it has no local part.
*
* @return True, if the JID is a domain JID, i.e. if it has no local part.
*/
boolean isDomainJid();
/**
* Gets the bare JID representation of this JID, i.e. removes the resource part.
* <blockquote>
* <p>The term "bare JID" refers to an XMPP address of the form &lt;localpart@domainpart&gt; (for an account at a server) or of the form &lt;domainpart&gt; (for a server).</p>
* </blockquote>
*
* @return The bare JID.
* @see #withResource(CharSequence)
*/
Jid asBareJid();
/**
* Creates a new JID with a new local part and the same domain and resource part of the current JID.
*
* @param local The local part.
* @return The JID with a new local part.
* @throws IllegalArgumentException If the local is not a valid local part.
* @see #withResource(CharSequence)
*/
Jid withLocal(CharSequence local);
/**
* Creates a new full JID with a resource and the same local and domain part of the current JID.
*
* @param resource The resource.
* @return The full JID with a resource.
* @throws IllegalArgumentException If the resource is not a valid resource part.
* @see #asBareJid()
* @see #withLocal(CharSequence)
*/
Jid withResource(CharSequence resource);
/**
* Creates a new JID at a subdomain and at the same domain as this JID.
*
* @param subdomain The subdomain.
* @return The JID at a subdomain.
* @throws NullPointerException If subdomain is null.
* @throws IllegalArgumentException If subdomain is not a valid subdomain name.
*/
Jid atSubdomain(CharSequence subdomain);
/**
* Gets the local part of the JID, also known as the name or node.
* <blockquote>
* <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.3">3.3. Localpart</a></cite></p>
* <p>The localpart of a JID is an optional identifier placed before the
* domainpart and separated from the latter by the '@' character.
* Typically, a localpart uniquely identifies the entity requesting and
* using network access provided by a server (i.e., a local account),
* although it can also represent other kinds of entities (e.g., a
* chatroom associated with a multi-user chat service [XEP-0045]). The
* entity represented by an XMPP localpart is addressed within the
* context of a specific domain (i.e., &lt;localpart@domainpart&gt;).</p>
* </blockquote>
*
* @return The local part or null.
* @see #getEscapedLocal()
*/
String getLocal();
/**
* Gets the escaped local part of the JID.
*
* @return The escaped local part or null.
* @see #getLocal()
* @since 0.8.0
*/
String getEscapedLocal();
/**
* Gets the domain part.
* <blockquote>
* <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.2">3.2. Domainpart</a></cite></p>
* <p>The domainpart is the primary identifier and is the only REQUIRED
* element of a JID (a mere domainpart is a valid JID). Typically,
* a domainpart identifies the "home" server to which clients connect
* for XML routing and data management functionality.</p>
* </blockquote>
*
* @return The domain part.
*/
String getDomain();
/**
* Gets the resource part.
* <blockquote>
* <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.4">3.4. Resourcepart</a></cite></p>
* <p>The resourcepart of a JID is an optional identifier placed after the
* domainpart and separated from the latter by the '/' character. A
* resourcepart can modify either a &lt;localpart@domainpart&gt; address or a
* mere &lt;domainpart&gt; address. Typically, a resourcepart uniquely
* identifies a specific connection (e.g., a device or location) or
* object (e.g., an occupant in a multi-user chatroom [XEP-0045])
* belonging to the entity associated with an XMPP localpart at a domain
* (i.e., &lt;localpart@domainpart/resourcepart&gt;).</p>
* </blockquote>
*
* @return The resource part or null.
*/
String getResource();
/**
* Returns the JID in escaped form as described in <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>.
*
* @return The escaped JID.
* @see #toString()
*/
String toEscapedString();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -1 +1,2 @@
include ':libs:xmpp-addr'
rootProject.name = 'Conversations'

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/your_photo"
android:orientation="vertical">
android:orientation="vertical"
android:layout_toRightOf="@+id/your_photo">
<RelativeLayout
android:layout_width="match_parent"
@ -67,7 +70,9 @@
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_toStartOf="@+id/edit_muc_name_button"
android:orientation="vertical">
android:orientation="vertical"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/edit_muc_name_button">
<TextView
android:id="@+id/muc_title"
@ -91,7 +96,9 @@
android:layout_alignParentStart="true"
android:layout_toStartOf="@+id/edit_muc_name_button"
android:orientation="vertical"
android:visibility="gone">
android:visibility="gone"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/edit_muc_name_button">
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
@ -132,7 +139,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" />
</RelativeLayout>
</LinearLayout>
</RelativeLayout>
@ -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" />
<ImageButton
android:id="@+id/change_conference_button"
@ -164,7 +173,8 @@
android:alpha="?attr/icon_alpha"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/image_button_padding"
android:src="?attr/icon_settings"/>
android:src="?attr/icon_settings"
android:layout_alignParentRight="true" />
</RelativeLayout>
<TableLayout
@ -193,7 +203,8 @@
android:layout_height="wrap_content"
android:layout_gravity="end"
android:paddingStart="4dp"
android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
android:textAppearance="@style/TextAppearance.Conversations.Body1"
android:paddingLeft="4dp" />
</TableRow>
</TableLayout>
@ -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" />
</RelativeLayout>
<RelativeLayout
@ -318,7 +330,8 @@
android:layout_toStartOf="@+id/notification_status_button"
android:text="@string/notify_on_all_messages"
android:textAppearance="@style/TextAppearance.Conversations.Body1"
/>
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/notification_status_button" />
<ImageButton
android:id="@+id/notification_status_button"
@ -331,7 +344,8 @@
android:alpha="?attr/icon_alpha"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/image_button_padding"
android:src="?attr/icon_notifications"/>
android:src="?attr/icon_notifications"
android:layout_alignParentRight="true" />
</RelativeLayout>
<TextView

View File

@ -29,7 +29,8 @@
android:layout_margin="16dp"
app:backgroundTint="?colorPrimary"
android:contentDescription="@string/action_unfix_from_location"
android:src="?attr/icon_gps_fixed"/>
android:src="?attr/icon_gps_fixed"
android:layout_alignParentRight="true" />
<org.osmdroid.views.MapView

View File

@ -28,6 +28,7 @@
android:contentDescription="@string/action_unfix_from_location"
android:src="?attr/icon_directions"
app:backgroundTint="?colorPrimary"
app:tint="@color/white"/>
app:tint="@color/white"
android:layout_alignParentRight="true" />
</RelativeLayout>
</layout>

View File

@ -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">
</ListView>
<android.support.design.widget.FloatingActionButton
@ -35,7 +36,8 @@
android:src="?attr/icon_scroll_down"
app:fabSize="mini"
android:visibility="gone"
app:useCompatPadding="true"/>
app:useCompatPadding="true"
android:layout_alignParentRight="true" />
<eu.siacs.conversations.ui.widget.UnreadCountCustomView
android:id="@+id/unread_count_custom_view"
@ -47,7 +49,10 @@
android:layout_marginEnd="8dp"
android:elevation="8dp"
android:visibility="gone"
app:backgroundColor="?attr/unread_count" />
app:backgroundColor="?attr/unread_count"
android:layout_alignRight="@+id/scroll_to_bottom_button"
tools:ignore="RtlCompat"
android:layout_marginRight="8dp" />
<RelativeLayout
android:id="@+id/textsend"
@ -55,7 +60,8 @@
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:background="?attr/color_background_primary">
android:background="?attr/color_background_primary"
android:layout_alignParentLeft="true">
<android.support.v7.widget.RecyclerView
android:visibility="gone"
@ -68,7 +74,9 @@
android:orientation="horizontal"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
android:paddingTop="8dp"
android:requiresFadingEdge="horizontal">
android:requiresFadingEdge="horizontal"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/textSendButton">
</android.support.v7.widget.RecyclerView>
@ -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">
<requestFocus/>
</eu.siacs.conversations.ui.widget.EditMessage>
@ -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" />
</RelativeLayout>
<RelativeLayout
@ -125,7 +136,10 @@
android:layout_centerVertical="true"
android:layout_toStartOf="@+id/snackbar_action"
android:paddingStart="24dp"
android:textAppearance="@style/TextAppearance.Conversations.Body1.OnDark"/>
android:textAppearance="@style/TextAppearance.Conversations.Body1.OnDark"
android:layout_alignParentLeft="true"
android:paddingLeft="24dp"
android:layout_toLeftOf="@+id/snackbar_action" />
<TextView
android:id="@+id/snackbar_action"
@ -139,7 +153,8 @@
android:paddingTop="16dp"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.Conversations.Body1.OnDark"
android:textStyle="bold"/>
android:textStyle="bold"
android:layout_alignParentRight="true" />
</RelativeLayout>
</RelativeLayout>

View File

@ -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" />
</RelativeLayout>
</layout>

View File

@ -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">
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/message_photo"
android:layout_width="48dp"