Jid.java

  1package eu.siacs.conversations.xmpp.jid;
  2
  3import android.util.LruCache;
  4
  5import net.java.otr4j.session.SessionID;
  6
  7import java.net.IDN;
  8
  9import gnu.inet.encoding.Stringprep;
 10import gnu.inet.encoding.StringprepException;
 11
 12/**
 13 * The `Jid' class provides an immutable representation of a JID.
 14 */
 15public final class Jid {
 16
 17	private static LruCache<String,Jid> cache = new LruCache<>(1024);
 18
 19	private final String localpart;
 20	private final String domainpart;
 21	private final String resourcepart;
 22
 23	// It's much more efficient to store the ful JID as well as the parts instead of figuring them
 24	// all out every time (since some characters are displayed but aren't used for comparisons).
 25	private final String displayjid;
 26
 27	public String getLocalpart() {
 28		return localpart;
 29	}
 30
 31	public String getDomainpart() {
 32		return IDN.toUnicode(domainpart);
 33	}
 34
 35	public String getResourcepart() {
 36		return resourcepart;
 37	}
 38
 39	public static Jid fromSessionID(final SessionID id) throws InvalidJidException{
 40		if (id.getUserID().isEmpty()) {
 41			return Jid.fromString(id.getAccountID());
 42		} else {
 43			return Jid.fromString(id.getAccountID()+"/"+id.getUserID());
 44		}
 45	}
 46
 47	public static Jid fromString(final String jid) throws InvalidJidException {
 48		return new Jid(jid);
 49	}
 50
 51	public static Jid fromParts(final String localpart,
 52			final String domainpart,
 53			final String resourcepart) throws InvalidJidException {
 54		String out;
 55		if (localpart == null || localpart.isEmpty()) {
 56			out = domainpart;
 57		} else {
 58			out = localpart + "@" + domainpart;
 59		}
 60		if (resourcepart != null && !resourcepart.isEmpty()) {
 61			out = out + "/" + resourcepart;
 62		}
 63		return new Jid(out);
 64	}
 65
 66	private Jid(final String jid) throws InvalidJidException {
 67		if (jid == null) throw new InvalidJidException(InvalidJidException.IS_NULL);
 68
 69		Jid fromCache = Jid.cache.get(jid);
 70		if (fromCache != null) {
 71			displayjid = fromCache.displayjid;
 72			localpart = fromCache.localpart;
 73			domainpart = fromCache.domainpart;
 74			resourcepart = fromCache.resourcepart;
 75			return;
 76		}
 77
 78		// Hackish Android way to count the number of chars in a string... should work everywhere.
 79		final int atCount = jid.length() - jid.replace("@", "").length();
 80		final int slashCount = jid.length() - jid.replace("/", "").length();
 81
 82		// Throw an error if there's anything obvious wrong with the JID...
 83		if (jid.isEmpty() || jid.length() > 3071) {
 84			throw new InvalidJidException(InvalidJidException.INVALID_LENGTH);
 85		}
 86
 87		// Go ahead and check if the localpart or resourcepart is empty.
 88		if (jid.startsWith("@") || (jid.endsWith("@") && slashCount == 0) || jid.startsWith("/") || (jid.endsWith("/") && slashCount < 2)) {
 89			throw new InvalidJidException(InvalidJidException.INVALID_CHARACTER);
 90		}
 91
 92		String finaljid;
 93
 94		final int domainpartStart;
 95		final int atLoc = jid.indexOf("@");
 96		final int slashLoc = jid.indexOf("/");
 97		// If there is no "@" in the JID (eg. "example.net" or "example.net/resource")
 98		// or there are one or more "@" signs but they're all in the resourcepart (eg. "example.net/@/rp@"):
 99		if (atCount == 0 || (atCount > 0 && slashLoc != -1 && atLoc > slashLoc)) {
100			localpart = "";
101			finaljid = "";
102			domainpartStart = 0;
103		} else {
104			final String lp = jid.substring(0, atLoc);
105			try {
106				localpart = Stringprep.nodeprep(lp);
107			} catch (final StringprepException e) {
108				throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
109			}
110			if (localpart.isEmpty() || localpart.length() > 1023) {
111				throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
112			}
113			domainpartStart = atLoc + 1;
114			finaljid = lp + "@";
115		}
116
117		final String dp;
118		if (slashCount > 0) {
119			final String rp = jid.substring(slashLoc + 1, jid.length());
120			try {
121				resourcepart = Stringprep.resourceprep(rp);
122			} catch (final StringprepException e) {
123				throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
124			}
125			if (resourcepart.isEmpty() || resourcepart.length() > 1023) {
126				throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
127			}
128			dp = IDN.toUnicode(jid.substring(domainpartStart, slashLoc), IDN.USE_STD3_ASCII_RULES);
129			finaljid = finaljid + dp + "/" + rp;
130		} else {
131			resourcepart = "";
132			dp = IDN.toUnicode(jid.substring(domainpartStart, jid.length()),
133					IDN.USE_STD3_ASCII_RULES);
134			finaljid = finaljid + dp;
135		}
136
137		// Remove trailing "." before storing the domain part.
138		if (dp.endsWith(".")) {
139			try {
140				domainpart = IDN.toASCII(dp.substring(0, dp.length() - 1), IDN.USE_STD3_ASCII_RULES);
141			} catch (final IllegalArgumentException e) {
142				throw new InvalidJidException(e);
143			}
144		} else {
145			try {
146				domainpart = IDN.toASCII(dp, IDN.USE_STD3_ASCII_RULES);
147			} catch (final IllegalArgumentException e) {
148				throw new InvalidJidException(e);
149			}
150		}
151
152		// TODO: Find a proper domain validation library; validate individual parts, separators, etc.
153		if (domainpart.isEmpty() || domainpart.length() > 1023) {
154			throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
155		}
156
157		Jid.cache.put(jid,this);
158
159		this.displayjid = finaljid;
160	}
161
162	public Jid toBareJid() {
163		try {
164			return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, "");
165		} catch (final InvalidJidException e) {
166			// This should never happen.
167			return null;
168		}
169	}
170
171	public Jid toDomainJid() {
172		try {
173			return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart());
174		} catch (final InvalidJidException e) {
175			// This should never happen.
176			return null;
177		}
178	}
179
180	@Override
181	public String toString() {
182		return displayjid;
183	}
184
185	@Override
186	public boolean equals(final Object o) {
187		if (this == o) return true;
188		if (o == null || getClass() != o.getClass()) return false;
189
190		final Jid jid = (Jid) o;
191
192		return jid.hashCode() == this.hashCode();
193	}
194
195	@Override
196	public int hashCode() {
197		int result = localpart.hashCode();
198		result = 31 * result + domainpart.hashCode();
199		result = 31 * result + resourcepart.hashCode();
200		return result;
201	}
202
203	public boolean hasLocalpart() {
204		return !localpart.isEmpty();
205	}
206
207	public boolean isBareJid() {
208		return this.resourcepart.isEmpty();
209	}
210
211	public boolean isDomainJid() {
212		return !this.hasLocalpart();
213	}
214}