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