1
2
3
4
5 package com.jcabi.urn;
6
7 import com.jcabi.aspects.Immutable;
8 import java.io.Serializable;
9 import java.io.UnsupportedEncodingException;
10 import java.net.URI;
11 import java.net.URISyntaxException;
12 import java.net.URLDecoder;
13 import java.util.Map;
14 import java.util.Objects;
15 import java.util.TreeMap;
16 import org.apache.commons.lang3.StringUtils;
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 @Immutable
37 @SuppressWarnings({
38 "PMD.TooManyMethods", "PMD.GodClass",
39 "PMD.OnlyOneConstructorShouldDoInitialization",
40 "PMD.ConstructorOnlyInitializesOrCallOtherConstructors"
41 })
42 public final class URN implements Comparable<URN>, Serializable {
43
44
45
46
47 private static final long serialVersionUID = 0xBF46AFCD9612A6DFL;
48
49
50
51
52 private static final String ENCODING = "UTF-8";
53
54
55
56
57 private static final String EMPTY = "void";
58
59
60
61
62 private static final String PREFIX = "urn";
63
64
65
66
67 private static final String SEP = ":";
68
69
70
71
72 private static final String REGEX =
73
74 "^(?i)^urn(?-i):[a-z]{1,31}(:([\\-a-zA-Z0-9/]|%[0-9a-fA-F]{2})*)+(\\?\\w+(=([\\-a-zA-Z0-9/]|%[0-9a-fA-F]{2})*)?(&\\w+(=([\\-a-zA-Z0-9/]|%[0-9a-fA-F]{2})*)?)*)?\\*?$";
75
76
77
78
79 private final String uri;
80
81
82
83
84 public URN() {
85 this(URN.EMPTY, "");
86 }
87
88
89
90
91
92
93
94 public URN(final String text) throws URISyntaxException {
95 if (text == null) {
96 throw new IllegalArgumentException("text can't be NULL");
97 }
98 if (!text.matches(URN.REGEX)) {
99 throw new URISyntaxException(text, "Invalid format of URN");
100 }
101 this.uri = text;
102 this.validate();
103 }
104
105
106
107
108
109
110
111
112 public URN(final String nid, final String nss) {
113 if (nid == null) {
114 throw new IllegalArgumentException("NID can't be NULL");
115 }
116 if (nss == null) {
117 throw new IllegalArgumentException("NSS can't be NULL");
118 }
119 this.uri = String.format(
120 "%s%s%s%2$s%s",
121 URN.PREFIX,
122 URN.SEP,
123 nid,
124 URN.encode(nss)
125 );
126 try {
127 this.validate();
128 } catch (final URISyntaxException ex) {
129 throw new IllegalArgumentException(ex);
130 }
131 }
132
133
134
135
136
137
138
139 @SuppressWarnings("PMD.ProhibitPublicStaticMethods")
140 public static URN create(final String text) {
141 if (text == null) {
142 throw new IllegalArgumentException("URN can't be NULL");
143 }
144 try {
145
146 return new URN(text);
147 } catch (final URISyntaxException ex) {
148 throw new IllegalArgumentException(ex);
149 }
150 }
151
152 @Override
153 public String toString() {
154 return this.uri;
155 }
156
157 @Override
158 public boolean equals(final Object obj) {
159 final boolean result;
160 if (this == obj) {
161 result = true;
162 } else if (obj instanceof URN) {
163 result = Objects.equals(this.uri, ((URN) obj).uri);
164 } else {
165 result = false;
166 }
167 return result;
168 }
169
170 @Override
171 public int hashCode() {
172 return Objects.hashCode(this.uri);
173 }
174
175 @Override
176 public int compareTo(final URN urn) {
177 return this.uri.compareTo(urn.uri);
178 }
179
180
181
182
183
184
185 @SuppressWarnings("PMD.ProhibitPublicStaticMethods")
186 public static boolean isValid(final String text) {
187 boolean valid = true;
188 try {
189
190 new URN(text);
191 } catch (final URISyntaxException ex) {
192 valid = false;
193 }
194 return valid;
195 }
196
197
198
199
200
201
202 public boolean matches(final String pattern) {
203 if (pattern == null) {
204 throw new IllegalArgumentException("pattern can't be NULL");
205 }
206 boolean matches = false;
207 if (this.toString().equals(pattern)) {
208 matches = true;
209 } else if (pattern.endsWith("*")) {
210 matches = this.uri.startsWith(
211 pattern.substring(0, pattern.length() - 1)
212 );
213 }
214 return matches;
215 }
216
217
218
219
220
221 public boolean isEmpty() {
222 return URN.EMPTY.equals(this.nid());
223 }
224
225
226
227
228
229 public URI toURI() {
230 return URI.create(this.uri);
231 }
232
233
234
235
236
237 public String nid() {
238 return this.segment(1);
239 }
240
241
242
243
244
245 public String nss() {
246 try {
247 return URLDecoder.decode(this.segment(2), URN.ENCODING);
248 } catch (final UnsupportedEncodingException ex) {
249 throw new IllegalStateException(ex);
250 }
251 }
252
253
254
255
256
257 public Map<String, String> params() {
258 return URN.demap(this.toString());
259 }
260
261
262
263
264
265
266 public String param(final String name) {
267 if (name == null) {
268 throw new IllegalArgumentException("param name can't be NULL");
269 }
270 final Map<String, String> params = this.params();
271 if (!params.containsKey(name)) {
272 throw new IllegalArgumentException(
273 String.format(
274 "Param '%s' not found in '%s', among %s",
275 name,
276 this,
277 params.keySet()
278 )
279 );
280 }
281 return params.get(name);
282 }
283
284
285
286
287
288
289
290 public URN param(final String name, final Object value) {
291 if (name == null) {
292 throw new IllegalArgumentException("param can't be NULL");
293 }
294 if (value == null) {
295 throw new IllegalArgumentException("param value can't be NULL");
296 }
297 final Map<String, String> params = this.params();
298 params.put(name, value.toString());
299 return URN.create(
300 String.format(
301 "%s%s",
302 StringUtils.split(this.toString(), '?')[0],
303 URN.enmap(params)
304 )
305 );
306 }
307
308
309
310
311
312 public URN pure() {
313 String urn = this.toString();
314 if (this.hasParams()) {
315
316 urn = urn.substring(0, urn.indexOf('?'));
317 }
318 return URN.create(urn);
319 }
320
321
322
323
324
325 public boolean hasParams() {
326
327 return this.toString().contains("?");
328 }
329
330
331
332
333
334
335 private String segment(final int pos) {
336 return StringUtils.splitPreserveAllTokens(
337 this.uri,
338 URN.SEP,
339
340 3
341 )[pos];
342 }
343
344
345
346
347
348 private void validate() throws URISyntaxException {
349 if (this.isEmpty() && !this.nss().isEmpty()) {
350 throw new URISyntaxException(
351 this.toString(),
352 "Empty URN can't have NSS"
353 );
354 }
355 final String nid = this.nid();
356 if (!nid.matches("^[a-z]{1,31}$")) {
357 throw new IllegalArgumentException(
358 String.format(
359 "NID '%s' can contain up to 31 low case letters",
360 this.nid()
361 )
362 );
363 }
364 if (URN.PREFIX.equalsIgnoreCase(nid)) {
365 throw new IllegalArgumentException(
366 "NID can't be 'urn' according to RFC 2141, section 2.1"
367 );
368 }
369 }
370
371
372
373
374
375
376 private static Map<String, String> demap(final String urn) {
377 final Map<String, String> map = new TreeMap<>();
378 final String[] sectors = StringUtils.split(urn, '?');
379 if (sectors.length == 2) {
380 final String[] parts = StringUtils.split(sectors[1], '&');
381 for (final String part : parts) {
382 final String[] pair = StringUtils.split(part, '=');
383 final String value;
384 if (pair.length == 2) {
385 try {
386 value = URLDecoder.decode(pair[1], URN.ENCODING);
387 } catch (final UnsupportedEncodingException ex) {
388 throw new IllegalStateException(ex);
389 }
390 } else {
391 value = "";
392 }
393 map.put(pair[0], value);
394 }
395 }
396 return map;
397 }
398
399
400
401
402
403
404 private static String enmap(final Map<String, String> params) {
405 final StringBuilder query = new StringBuilder(100);
406 if (!params.isEmpty()) {
407 query.append('?');
408 boolean first = true;
409 for (final Map.Entry<String, String> param : params.entrySet()) {
410 if (!first) {
411 query.append('&');
412 }
413 query.append(param.getKey());
414 if (!param.getValue().isEmpty()) {
415 query.append('=').append(URN.encode(param.getValue()));
416 }
417 first = false;
418 }
419 }
420 return query.toString();
421 }
422
423
424
425
426
427
428 private static String encode(final String text) {
429 final byte[] bytes;
430 try {
431 bytes = text.getBytes(URN.ENCODING);
432 } catch (final UnsupportedEncodingException ex) {
433 throw new IllegalStateException(ex);
434 }
435 final StringBuilder encoded = new StringBuilder(100);
436 for (final byte chr : bytes) {
437 if (URN.allowed(chr)) {
438 encoded.append((char) chr);
439 } else {
440 encoded.append('%').append(String.format("%X", chr));
441 }
442 }
443 return encoded.toString();
444 }
445
446
447
448
449
450
451 private static boolean allowed(final byte chr) {
452
453 return chr >= 'A' && chr <= 'Z'
454 || chr >= '0' && chr <= '9'
455 || chr >= 'a' && chr <= 'z'
456 || chr == '/' || chr == '-';
457 }
458 }