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			dp = IDN.toUnicode(jid.substring(domainpartStart, slashLoc), IDN.USE_STD3_ASCII_RULES);
134			finaljid = finaljid + dp + "/" + rp;
135		} else {
136			resourcepart = "";
137			dp = IDN.toUnicode(jid.substring(domainpartStart, jid.length()),
138					IDN.USE_STD3_ASCII_RULES);
139			finaljid = finaljid + dp;
140		}
141
142		// Remove trailing "." before storing the domain part.
143		if (dp.endsWith(".")) {
144			try {
145				domainpart = IDN.toASCII(dp.substring(0, dp.length() - 1), IDN.USE_STD3_ASCII_RULES);
146			} catch (final IllegalArgumentException e) {
147				throw new InvalidJidException(e);
148			}
149		} else {
150			try {
151				domainpart = IDN.toASCII(dp, IDN.USE_STD3_ASCII_RULES);
152			} catch (final IllegalArgumentException e) {
153				throw new InvalidJidException(e);
154			}
155		}
156
157		// TODO: Find a proper domain validation library; validate individual parts, separators, etc.
158		if (domainpart.isEmpty() || domainpart.length() > 1023) {
159			throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
160		}
161
162		Jid.cache.put(jid, this);
163
164		this.displayjid = finaljid;
165	}
166
167	public Jid toBareJid() {
168		try {
169			return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, "");
170		} catch (final InvalidJidException e) {
171			// This should never happen.
172			return null;
173		}
174	}
175
176	public Jid toDomainJid() {
177		try {
178			return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart());
179		} catch (final InvalidJidException e) {
180			// This should never happen.
181			return null;
182		}
183	}
184
185	@Override
186	public String toString() {
187		return displayjid;
188	}
189
190	@Override
191	public boolean equals(final Object o) {
192		if (this == o) return true;
193		if (o == null || getClass() != o.getClass()) return false;
194
195		final Jid jid = (Jid) o;
196
197		return jid.hashCode() == this.hashCode();
198	}
199
200	@Override
201	public int hashCode() {
202		int result = localpart.hashCode();
203		result = 31 * result + domainpart.hashCode();
204		result = 31 * result + resourcepart.hashCode();
205		return result;
206	}
207
208	public boolean hasLocalpart() {
209		return !localpart.isEmpty();
210	}
211
212	public boolean isBareJid() {
213		return this.resourcepart.isEmpty();
214	}
215
216	public boolean isDomainJid() {
217		return !this.hasLocalpart();
218	}
219}