Jid.java

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