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