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 IDN.toUnicode(localpart);
23 }
24
25 public String getDomainpart() {
26 return IDN.toUnicode(domainpart);
27 }
28
29 public String getResourcepart() {
30 return IDN.toUnicode(resourcepart);
31 }
32
33 public 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;
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 domainpart = IDN.toASCII(dp.substring(0, dp.length() - 1), IDN.USE_STD3_ASCII_RULES);
112 } else {
113 domainpart = IDN.toASCII(dp, IDN.USE_STD3_ASCII_RULES);
114 }
115
116 // TODO: Find a proper domain validation library; validate individual parts, separators, etc.
117 if (domainpart.isEmpty() || domainpart.length() > 1023) {
118 throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
119 }
120
121 this.displayjid = finaljid;
122 }
123
124 public Jid getBareJid() {
125 try {
126 return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, "");
127 } catch (final InvalidJidException e) {
128 // This should never happen due to the contracts we have in place.
129 return null;
130 }
131 }
132
133 @Override
134 public String toString() {
135 return displayjid;
136 }
137
138 @Override
139 public boolean equals(final Object o) {
140 if (this == o) return true;
141 if (o == null || getClass() != o.getClass()) return false;
142
143 final Jid jid = (Jid) o;
144
145 return jid.hashCode() == this.hashCode();
146 }
147
148 @Override
149 public int hashCode() {
150 int result = localpart.hashCode();
151 result = 31 * result + domainpart.hashCode();
152 result = 31 * result + resourcepart.hashCode();
153 return result;
154 }
155}