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}