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}