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 new Jid(jid);
 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);
 65	}
 66
 67	private Jid(final String jid) 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			displayjid = fromCache.displayjid;
 73			localpart = fromCache.localpart;
 74			domainpart = fromCache.domainpart;
 75			resourcepart = fromCache.resourcepart;
 76			return;
 77		}
 78
 79		// Hackish Android way to count the number of chars in a string... should work everywhere.
 80		final int atCount = jid.length() - jid.replace("@", "").length();
 81		final int slashCount = jid.length() - jid.replace("/", "").length();
 82
 83		// Throw an error if there's anything obvious wrong with the JID...
 84		if (jid.isEmpty() || jid.length() > 3071) {
 85			throw new InvalidJidException(InvalidJidException.INVALID_LENGTH);
 86		}
 87
 88		// Go ahead and check if the localpart or resourcepart is empty.
 89		if (jid.startsWith("@") || (jid.endsWith("@") && slashCount == 0) || jid.startsWith("/") || (jid.endsWith("/") && slashCount < 2)) {
 90			throw new InvalidJidException(InvalidJidException.INVALID_CHARACTER);
 91		}
 92
 93		String finaljid;
 94
 95		final int domainpartStart;
 96		final int atLoc = jid.indexOf("@");
 97		final int slashLoc = jid.indexOf("/");
 98		// If there is no "@" in the JID (eg. "example.net" or "example.net/resource")
 99		// or there are one or more "@" signs but they're all in the resourcepart (eg. "example.net/@/rp@"):
100		if (atCount == 0 || (atCount > 0 && slashLoc != -1 && atLoc > slashLoc)) {
101			localpart = "";
102			finaljid = "";
103			domainpartStart = 0;
104		} else {
105			final String lp = jid.substring(0, atLoc);
106			try {
107				localpart = Config.DISABLE_STRING_PREP ? lp : Stringprep.nodeprep(lp);
108			} catch (final StringprepException e) {
109				throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
110			}
111			if (localpart.isEmpty() || localpart.length() > 1023) {
112				throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
113			}
114			domainpartStart = atLoc + 1;
115			finaljid = lp + "@";
116		}
117
118		final String dp;
119		if (slashCount > 0) {
120			final String rp = jid.substring(slashLoc + 1, jid.length());
121			try {
122				resourcepart = Config.DISABLE_STRING_PREP ? rp : Stringprep.resourceprep(rp);
123			} catch (final StringprepException e) {
124				throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
125			}
126			if (resourcepart.isEmpty() || resourcepart.length() > 1023) {
127				throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
128			}
129			dp = IDN.toUnicode(jid.substring(domainpartStart, slashLoc), IDN.USE_STD3_ASCII_RULES);
130			finaljid = finaljid + dp + "/" + rp;
131		} else {
132			resourcepart = "";
133			dp = IDN.toUnicode(jid.substring(domainpartStart, jid.length()),
134					IDN.USE_STD3_ASCII_RULES);
135			finaljid = finaljid + dp;
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		this.displayjid = finaljid;
161	}
162
163	public Jid toBareJid() {
164		try {
165			return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, "");
166		} catch (final InvalidJidException e) {
167			// This should never happen.
168			return null;
169		}
170	}
171
172	public Jid toDomainJid() {
173		try {
174			return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart());
175		} catch (final InvalidJidException e) {
176			// This should never happen.
177			return null;
178		}
179	}
180
181	@Override
182	public String toString() {
183		return displayjid;
184	}
185
186	@Override
187	public boolean equals(final Object o) {
188		if (this == o) return true;
189		if (o == null || getClass() != o.getClass()) return false;
190
191		final Jid jid = (Jid) o;
192
193		return jid.hashCode() == this.hashCode();
194	}
195
196	@Override
197	public int hashCode() {
198		int result = localpart.hashCode();
199		result = 31 * result + domainpart.hashCode();
200		result = 31 * result + resourcepart.hashCode();
201		return result;
202	}
203
204	public boolean hasLocalpart() {
205		return !localpart.isEmpty();
206	}
207
208	public boolean isBareJid() {
209		return this.resourcepart.isEmpty();
210	}
211
212	public boolean isDomainJid() {
213		return !this.hasLocalpart();
214	}
215}