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