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