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