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 eu.siacs.conversations.Config;
 10import gnu.inet.encoding.Stringprep;
 11import gnu.inet.encoding.StringprepException;
 12
 13/**
 14 * The `Jid' class provides an immutable representation of a JID.
 15 */
 16public final class Jid {
 17
 18	private static LruCache<String,Jid> cache = new LruCache<>(1024);
 19
 20	private final String localpart;
 21	private final String domainpart;
 22	private final String resourcepart;
 23
 24	public String getLocalpart() {
 25		return localpart;
 26	}
 27
 28	public String getDomainpart() {
 29		return IDN.toUnicode(domainpart);
 30	}
 31
 32	public String getResourcepart() {
 33		return resourcepart;
 34	}
 35
 36	public static Jid fromSessionID(final SessionID id) throws InvalidJidException{
 37		if (id.getUserID().isEmpty()) {
 38			return Jid.fromString(id.getAccountID());
 39		} else {
 40			return Jid.fromString(id.getAccountID()+"/"+id.getUserID());
 41		}
 42	}
 43
 44	public static Jid fromString(final String jid) throws InvalidJidException {
 45		return Jid.fromString(jid, false);
 46	}
 47
 48	public static Jid fromString(final String jid, final boolean safe) throws InvalidJidException {
 49		return new Jid(jid, safe);
 50	}
 51
 52	public static Jid fromParts(final String localpart,
 53			final String domainpart,
 54			final String resourcepart) throws InvalidJidException {
 55		String out;
 56		if (localpart == null || localpart.isEmpty()) {
 57			out = domainpart;
 58		} else {
 59			out = localpart + "@" + domainpart;
 60		}
 61		if (resourcepart != null && !resourcepart.isEmpty()) {
 62			out = out + "/" + resourcepart;
 63		}
 64		return new Jid(out, false);
 65	}
 66
 67	private Jid(final String jid, final boolean safe) throws InvalidJidException {
 68		if (jid == null) throw new InvalidJidException(InvalidJidException.IS_NULL);
 69
 70		Jid fromCache = Jid.cache.get(jid);
 71		if (fromCache != null) {
 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		final int domainpartStart;
 93		final int atLoc = jid.indexOf("@");
 94		final int slashLoc = jid.indexOf("/");
 95		// If there is no "@" in the JID (eg. "example.net" or "example.net/resource")
 96		// or there are one or more "@" signs but they're all in the resourcepart (eg. "example.net/@/rp@"):
 97		if (atCount == 0 || (atCount > 0 && slashLoc != -1 && atLoc > slashLoc)) {
 98			localpart = "";
 99			domainpartStart = 0;
100		} else {
101			final String lp = jid.substring(0, atLoc);
102			try {
103				localpart = Config.DISABLE_STRING_PREP || safe ? lp : Stringprep.nodeprep(lp);
104			} catch (final StringprepException e) {
105				throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
106			}
107			if (localpart.isEmpty() || localpart.length() > 1023) {
108				throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
109			}
110			domainpartStart = atLoc + 1;
111		}
112
113		final String dp;
114		if (slashCount > 0) {
115			final String rp = jid.substring(slashLoc + 1, jid.length());
116			try {
117				resourcepart = Config.DISABLE_STRING_PREP || safe ? rp : Stringprep.resourceprep(rp);
118			} catch (final StringprepException e) {
119				throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
120			}
121			if (resourcepart.isEmpty() || resourcepart.length() > 1023) {
122				throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
123			}
124			try {
125				dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, slashLoc)), IDN.USE_STD3_ASCII_RULES);
126			} catch (final StringprepException e) {
127				throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
128			}
129		} else {
130			resourcepart = "";
131			try{
132				dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, jid.length())), IDN.USE_STD3_ASCII_RULES);
133			} catch (final StringprepException e) {
134				throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
135			}
136		}
137
138		// Remove trailing "." before storing the domain part.
139		if (dp.endsWith(".")) {
140			try {
141				domainpart = IDN.toASCII(dp.substring(0, dp.length() - 1), IDN.USE_STD3_ASCII_RULES);
142			} catch (final IllegalArgumentException e) {
143				throw new InvalidJidException(e);
144			}
145		} else {
146			try {
147				domainpart = IDN.toASCII(dp, IDN.USE_STD3_ASCII_RULES);
148			} catch (final IllegalArgumentException e) {
149				throw new InvalidJidException(e);
150			}
151		}
152
153		// TODO: Find a proper domain validation library; validate individual parts, separators, etc.
154		if (domainpart.isEmpty() || domainpart.length() > 1023) {
155			throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
156		}
157
158		Jid.cache.put(jid, this);
159	}
160
161	public Jid toBareJid() {
162		try {
163			return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, "");
164		} catch (final InvalidJidException e) {
165			// This should never happen.
166			throw new AssertionError("Jid " + this.toString() + " invalid");
167		}
168	}
169
170	public Jid toDomainJid() {
171		try {
172			return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart());
173		} catch (final InvalidJidException e) {
174			// This should never happen.
175			throw new AssertionError("Jid " + this.toString() + " invalid");
176		}
177	}
178
179	@Override
180	public String toString() {
181		String out;
182		if (hasLocalpart()) {
183			out = localpart + '@' + domainpart;
184		} else {
185			out = domainpart;
186		}
187		if (!resourcepart.isEmpty()) {
188			out += '/'+resourcepart;
189		}
190		return out;
191	}
192
193	@Override
194	public boolean equals(final Object o) {
195		if (this == o) return true;
196		if (o == null || getClass() != o.getClass()) return false;
197
198		final Jid jid = (Jid) o;
199
200		return jid.hashCode() == this.hashCode();
201	}
202
203	@Override
204	public int hashCode() {
205		int result = localpart.hashCode();
206		result = 31 * result + domainpart.hashCode();
207		result = 31 * result + resourcepart.hashCode();
208		return result;
209	}
210
211	public boolean hasLocalpart() {
212		return !localpart.isEmpty();
213	}
214
215	public boolean isBareJid() {
216		return this.resourcepart.isEmpty();
217	}
218
219	public boolean isDomainJid() {
220		return !this.hasLocalpart();
221	}
222}