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