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 public String getLocalpart() {
25 return localpart;
26 }
27
28 public String getDomainpart() {
29 return IDN.toUnicode(domainpart);
30 }
31
32 public String getResourcepart() {
33 return resourcepart;
34 }
35
36 public static Jid fromSessionID(final SessionID id) throws InvalidJidException{
37 if (id.getUserID().isEmpty()) {
38 return Jid.fromString(id.getAccountID());
39 } else {
40 return Jid.fromString(id.getAccountID()+"/"+id.getUserID());
41 }
42 }
43
44 public static Jid fromString(final String jid) throws InvalidJidException {
45 return Jid.fromString(jid, false);
46 }
47
48 public static Jid fromString(final String jid, final boolean safe) throws InvalidJidException {
49 return new Jid(jid, safe);
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, false);
65 }
66
67 private Jid(final String jid, final boolean safe) 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 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 final int domainpartStart;
93 final int atLoc = jid.indexOf("@");
94 final int slashLoc = jid.indexOf("/");
95 // If there is no "@" in the JID (eg. "example.net" or "example.net/resource")
96 // or there are one or more "@" signs but they're all in the resourcepart (eg. "example.net/@/rp@"):
97 if (atCount == 0 || (atCount > 0 && slashLoc != -1 && atLoc > slashLoc)) {
98 localpart = "";
99 domainpartStart = 0;
100 } else {
101 final String lp = jid.substring(0, atLoc);
102 try {
103 localpart = Config.DISABLE_STRING_PREP || safe ? lp : Stringprep.nodeprep(lp);
104 } catch (final StringprepException e) {
105 throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
106 }
107 if (localpart.isEmpty() || localpart.length() > 1023) {
108 throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
109 }
110 domainpartStart = atLoc + 1;
111 }
112
113 final String dp;
114 if (slashCount > 0) {
115 final String rp = jid.substring(slashLoc + 1, jid.length());
116 try {
117 resourcepart = Config.DISABLE_STRING_PREP || safe ? rp : Stringprep.resourceprep(rp);
118 } catch (final StringprepException e) {
119 throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
120 }
121 if (resourcepart.isEmpty() || resourcepart.length() > 1023) {
122 throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
123 }
124 try {
125 dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, slashLoc)), IDN.USE_STD3_ASCII_RULES);
126 } catch (final StringprepException e) {
127 throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
128 }
129 } else {
130 resourcepart = "";
131 try{
132 dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, jid.length())), IDN.USE_STD3_ASCII_RULES);
133 } catch (final StringprepException e) {
134 throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
135 }
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
161 public Jid toBareJid() {
162 try {
163 return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, "");
164 } catch (final InvalidJidException e) {
165 // This should never happen.
166 throw new AssertionError("Jid " + this.toString() + " invalid");
167 }
168 }
169
170 public Jid toDomainJid() {
171 try {
172 return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart());
173 } catch (final InvalidJidException e) {
174 // This should never happen.
175 throw new AssertionError("Jid " + this.toString() + " invalid");
176 }
177 }
178
179 @Override
180 public String toString() {
181 String out;
182 if (hasLocalpart()) {
183 out = localpart + '@' + domainpart;
184 } else {
185 out = domainpart;
186 }
187 if (!resourcepart.isEmpty()) {
188 out += '/'+resourcepart;
189 }
190 return out;
191 }
192
193 @Override
194 public boolean equals(final Object o) {
195 if (this == o) return true;
196 if (o == null || getClass() != o.getClass()) return false;
197
198 final Jid jid = (Jid) o;
199
200 return jid.hashCode() == this.hashCode();
201 }
202
203 @Override
204 public int hashCode() {
205 int result = localpart.hashCode();
206 result = 31 * result + domainpart.hashCode();
207 result = 31 * result + resourcepart.hashCode();
208 return result;
209 }
210
211 public boolean hasLocalpart() {
212 return !localpart.isEmpty();
213 }
214
215 public boolean isBareJid() {
216 return this.resourcepart.isEmpty();
217 }
218
219 public boolean isDomainJid() {
220 return !this.hasLocalpart();
221 }
222}