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