1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.util; |
7 | |
8 | import java.lang.ref.SoftReference; |
9 | import java.net.URLEncoder; |
10 | import java.util.ArrayList; |
11 | import java.util.Locale; |
12 | |
13 | import org.h2.api.ErrorCode; |
14 | import org.h2.engine.Constants; |
15 | import org.h2.engine.SysProperties; |
16 | import org.h2.message.DbException; |
17 | |
18 | /** |
19 | * A few String utility functions. |
20 | */ |
21 | public class StringUtils { |
22 | |
23 | private static SoftReference<String[]> softCache = |
24 | new SoftReference<String[]>(null); |
25 | private static long softCacheCreated; |
26 | |
27 | private static final char[] HEX = "0123456789abcdef".toCharArray(); |
28 | private static final int[] HEX_DECODE = new int['f' + 1]; |
29 | |
30 | // memory used by this cache: |
31 | // 4 * 1024 * 2 (strings per pair) * 64 * 2 (bytes per char) = 0.5 MB |
32 | private static final int TO_UPPER_CACHE_LENGTH = 2 * 1024; |
33 | private static final int TO_UPPER_CACHE_MAX_ENTRY_LENGTH = 64; |
34 | private static final String[][] TO_UPPER_CACHE = new String[TO_UPPER_CACHE_LENGTH][]; |
35 | |
36 | static { |
37 | for (int i = 0; i < HEX_DECODE.length; i++) { |
38 | HEX_DECODE[i] = -1; |
39 | } |
40 | for (int i = 0; i <= 9; i++) { |
41 | HEX_DECODE[i + '0'] = i; |
42 | } |
43 | for (int i = 0; i <= 5; i++) { |
44 | HEX_DECODE[i + 'a'] = HEX_DECODE[i + 'A'] = i + 10; |
45 | } |
46 | } |
47 | |
48 | private StringUtils() { |
49 | // utility class |
50 | } |
51 | |
52 | private static String[] getCache() { |
53 | String[] cache; |
54 | // softCache can be null due to a Tomcat problem |
55 | // a workaround is disable the system property org.apache. |
56 | // catalina.loader.WebappClassLoader.ENABLE_CLEAR_REFERENCES |
57 | if (softCache != null) { |
58 | cache = softCache.get(); |
59 | if (cache != null) { |
60 | return cache; |
61 | } |
62 | } |
63 | // create a new cache at most every 5 seconds |
64 | // so that out of memory exceptions are not delayed |
65 | long time = System.currentTimeMillis(); |
66 | if (softCacheCreated != 0 && time - softCacheCreated < 5000) { |
67 | return null; |
68 | } |
69 | try { |
70 | cache = new String[SysProperties.OBJECT_CACHE_SIZE]; |
71 | softCache = new SoftReference<String[]>(cache); |
72 | return cache; |
73 | } finally { |
74 | softCacheCreated = System.currentTimeMillis(); |
75 | } |
76 | } |
77 | |
78 | /** |
79 | * Check if two strings are equal. Here, null is equal to null. |
80 | * |
81 | * @param a the first value |
82 | * @param b the second value |
83 | * @return true if both are null or both are equal |
84 | */ |
85 | public static boolean equals(String a, String b) { |
86 | if (a == null) { |
87 | return b == null; |
88 | } |
89 | return a.equals(b); |
90 | } |
91 | |
92 | /** |
93 | * Convert a string to uppercase using the English locale. |
94 | * |
95 | * @param s the test to convert |
96 | * @return the uppercase text |
97 | */ |
98 | public static String toUpperEnglish(String s) { |
99 | if (s.length() > TO_UPPER_CACHE_MAX_ENTRY_LENGTH) { |
100 | return s.toUpperCase(Locale.ENGLISH); |
101 | } |
102 | int index = s.hashCode() & (TO_UPPER_CACHE_LENGTH - 1); |
103 | String[] e = TO_UPPER_CACHE[index]; |
104 | if (e != null) { |
105 | if (e[0].equals(s)) { |
106 | return e[1]; |
107 | } |
108 | } |
109 | String s2 = s.toUpperCase(Locale.ENGLISH); |
110 | e = new String[] { s, s2 }; |
111 | TO_UPPER_CACHE[index] = e; |
112 | return s2; |
113 | } |
114 | |
115 | /** |
116 | * Convert a string to lowercase using the English locale. |
117 | * |
118 | * @param s the text to convert |
119 | * @return the lowercase text |
120 | */ |
121 | public static String toLowerEnglish(String s) { |
122 | return s.toLowerCase(Locale.ENGLISH); |
123 | } |
124 | |
125 | /** |
126 | * Check is a string starts with another string, ignoring the case. |
127 | * |
128 | * @param s the string to check (must be longer than start) |
129 | * @param start the prefix of s |
130 | * @return true if start is a prefix of s |
131 | */ |
132 | public static boolean startsWithIgnoreCase(String s, String start) { |
133 | if (s.length() < start.length()) { |
134 | return false; |
135 | } |
136 | return s.substring(0, start.length()).equalsIgnoreCase(start); |
137 | } |
138 | |
139 | /** |
140 | * Convert a string to a SQL literal. Null is converted to NULL. The text is |
141 | * enclosed in single quotes. If there are any special characters, the |
142 | * method STRINGDECODE is used. |
143 | * |
144 | * @param s the text to convert. |
145 | * @return the SQL literal |
146 | */ |
147 | public static String quoteStringSQL(String s) { |
148 | if (s == null) { |
149 | return "NULL"; |
150 | } |
151 | int length = s.length(); |
152 | StringBuilder buff = new StringBuilder(length + 2); |
153 | buff.append('\''); |
154 | for (int i = 0; i < length; i++) { |
155 | char c = s.charAt(i); |
156 | if (c == '\'') { |
157 | buff.append(c); |
158 | } else if (c < ' ' || c > 127) { |
159 | // need to start from the beginning because maybe there was a \ |
160 | // that was not quoted |
161 | return "STRINGDECODE(" + quoteStringSQL(javaEncode(s)) + ")"; |
162 | } |
163 | buff.append(c); |
164 | } |
165 | buff.append('\''); |
166 | return buff.toString(); |
167 | } |
168 | |
169 | /** |
170 | * Convert a string to a Java literal using the correct escape sequences. |
171 | * The literal is not enclosed in double quotes. The result can be used in |
172 | * properties files or in Java source code. |
173 | * |
174 | * @param s the text to convert |
175 | * @return the Java representation |
176 | */ |
177 | public static String javaEncode(String s) { |
178 | int length = s.length(); |
179 | StringBuilder buff = new StringBuilder(length); |
180 | for (int i = 0; i < length; i++) { |
181 | char c = s.charAt(i); |
182 | switch (c) { |
183 | // case '\b': |
184 | // // BS backspace |
185 | // // not supported in properties files |
186 | // buff.append("\\b"); |
187 | // break; |
188 | case '\t': |
189 | // HT horizontal tab |
190 | buff.append("\\t"); |
191 | break; |
192 | case '\n': |
193 | // LF linefeed |
194 | buff.append("\\n"); |
195 | break; |
196 | case '\f': |
197 | // FF form feed |
198 | buff.append("\\f"); |
199 | break; |
200 | case '\r': |
201 | // CR carriage return |
202 | buff.append("\\r"); |
203 | break; |
204 | case '"': |
205 | // double quote |
206 | buff.append("\\\""); |
207 | break; |
208 | case '\\': |
209 | // backslash |
210 | buff.append("\\\\"); |
211 | break; |
212 | default: |
213 | int ch = c & 0xffff; |
214 | if (ch >= ' ' && (ch < 0x80)) { |
215 | buff.append(c); |
216 | // not supported in properties files |
217 | // } else if (ch < 0xff) { |
218 | // buff.append("\\"); |
219 | // // make sure it's three characters (0x200 is octal 1000) |
220 | // buff.append(Integer.toOctalString(0x200 | ch).substring(1)); |
221 | } else { |
222 | buff.append("\\u"); |
223 | String hex = Integer.toHexString(ch); |
224 | // make sure it's four characters |
225 | for (int len = hex.length(); len < 4; len++) { |
226 | buff.append('0'); |
227 | } |
228 | buff.append(hex); |
229 | } |
230 | } |
231 | } |
232 | return buff.toString(); |
233 | } |
234 | |
235 | /** |
236 | * Add an asterisk ('[*]') at the given position. This format is used to |
237 | * show where parsing failed in a statement. |
238 | * |
239 | * @param s the text |
240 | * @param index the position |
241 | * @return the text with asterisk |
242 | */ |
243 | public static String addAsterisk(String s, int index) { |
244 | if (s != null) { |
245 | index = Math.min(index, s.length()); |
246 | s = s.substring(0, index) + "[*]" + s.substring(index); |
247 | } |
248 | return s; |
249 | } |
250 | |
251 | private static DbException getFormatException(String s, int i) { |
252 | return DbException.get(ErrorCode.STRING_FORMAT_ERROR_1, addAsterisk(s, i)); |
253 | } |
254 | |
255 | /** |
256 | * Decode a text that is encoded as a Java string literal. The Java |
257 | * properties file format and Java source code format is supported. |
258 | * |
259 | * @param s the encoded string |
260 | * @return the string |
261 | */ |
262 | public static String javaDecode(String s) { |
263 | int length = s.length(); |
264 | StringBuilder buff = new StringBuilder(length); |
265 | for (int i = 0; i < length; i++) { |
266 | char c = s.charAt(i); |
267 | if (c == '\\') { |
268 | if (i + 1 >= s.length()) { |
269 | throw getFormatException(s, i); |
270 | } |
271 | c = s.charAt(++i); |
272 | switch (c) { |
273 | case 't': |
274 | buff.append('\t'); |
275 | break; |
276 | case 'r': |
277 | buff.append('\r'); |
278 | break; |
279 | case 'n': |
280 | buff.append('\n'); |
281 | break; |
282 | case 'b': |
283 | buff.append('\b'); |
284 | break; |
285 | case 'f': |
286 | buff.append('\f'); |
287 | break; |
288 | case '#': |
289 | // for properties files |
290 | buff.append('#'); |
291 | break; |
292 | case '=': |
293 | // for properties files |
294 | buff.append('='); |
295 | break; |
296 | case ':': |
297 | // for properties files |
298 | buff.append(':'); |
299 | break; |
300 | case '"': |
301 | buff.append('"'); |
302 | break; |
303 | case '\\': |
304 | buff.append('\\'); |
305 | break; |
306 | case 'u': { |
307 | try { |
308 | c = (char) (Integer.parseInt(s.substring(i + 1, i + 5), 16)); |
309 | } catch (NumberFormatException e) { |
310 | throw getFormatException(s, i); |
311 | } |
312 | i += 4; |
313 | buff.append(c); |
314 | break; |
315 | } |
316 | default: |
317 | if (c >= '0' && c <= '9') { |
318 | try { |
319 | c = (char) (Integer.parseInt(s.substring(i, i + 3), 8)); |
320 | } catch (NumberFormatException e) { |
321 | throw getFormatException(s, i); |
322 | } |
323 | i += 2; |
324 | buff.append(c); |
325 | } else { |
326 | throw getFormatException(s, i); |
327 | } |
328 | } |
329 | } else { |
330 | buff.append(c); |
331 | } |
332 | } |
333 | return buff.toString(); |
334 | } |
335 | |
336 | /** |
337 | * Convert a string to the Java literal and enclose it with double quotes. |
338 | * Null will result in "null" (without double quotes). |
339 | * |
340 | * @param s the text to convert |
341 | * @return the Java representation |
342 | */ |
343 | public static String quoteJavaString(String s) { |
344 | if (s == null) { |
345 | return "null"; |
346 | } |
347 | return "\"" + javaEncode(s) + "\""; |
348 | } |
349 | |
350 | /** |
351 | * Convert a string array to the Java source code that represents this |
352 | * array. Null will be converted to 'null'. |
353 | * |
354 | * @param array the string array |
355 | * @return the Java source code (including new String[]{}) |
356 | */ |
357 | public static String quoteJavaStringArray(String[] array) { |
358 | if (array == null) { |
359 | return "null"; |
360 | } |
361 | StatementBuilder buff = new StatementBuilder("new String[]{"); |
362 | for (String a : array) { |
363 | buff.appendExceptFirst(", "); |
364 | buff.append(quoteJavaString(a)); |
365 | } |
366 | return buff.append('}').toString(); |
367 | } |
368 | |
369 | /** |
370 | * Convert an int array to the Java source code that represents this array. |
371 | * Null will be converted to 'null'. |
372 | * |
373 | * @param array the int array |
374 | * @return the Java source code (including new int[]{}) |
375 | */ |
376 | public static String quoteJavaIntArray(int[] array) { |
377 | if (array == null) { |
378 | return "null"; |
379 | } |
380 | StatementBuilder buff = new StatementBuilder("new int[]{"); |
381 | for (int a : array) { |
382 | buff.appendExceptFirst(", "); |
383 | buff.append(a); |
384 | } |
385 | return buff.append('}').toString(); |
386 | } |
387 | |
388 | /** |
389 | * Enclose a string with '(' and ')' if this is not yet done. |
390 | * |
391 | * @param s the string |
392 | * @return the enclosed string |
393 | */ |
394 | public static String enclose(String s) { |
395 | if (s.startsWith("(")) { |
396 | return s; |
397 | } |
398 | return "(" + s + ")"; |
399 | } |
400 | |
401 | /** |
402 | * Remove enclosing '(' and ')' if this text is enclosed. |
403 | * |
404 | * @param s the potentially enclosed string |
405 | * @return the string |
406 | */ |
407 | public static String unEnclose(String s) { |
408 | if (s.startsWith("(") && s.endsWith(")")) { |
409 | return s.substring(1, s.length() - 1); |
410 | } |
411 | return s; |
412 | } |
413 | |
414 | /** |
415 | * Encode the string as an URL. |
416 | * |
417 | * @param s the string to encode |
418 | * @return the encoded string |
419 | */ |
420 | public static String urlEncode(String s) { |
421 | try { |
422 | return URLEncoder.encode(s, "UTF-8"); |
423 | } catch (Exception e) { |
424 | // UnsupportedEncodingException |
425 | throw DbException.convert(e); |
426 | } |
427 | } |
428 | |
429 | /** |
430 | * Decode the URL to a string. |
431 | * |
432 | * @param encoded the encoded URL |
433 | * @return the decoded string |
434 | */ |
435 | public static String urlDecode(String encoded) { |
436 | int length = encoded.length(); |
437 | byte[] buff = new byte[length]; |
438 | int j = 0; |
439 | for (int i = 0; i < length; i++) { |
440 | char ch = encoded.charAt(i); |
441 | if (ch == '+') { |
442 | buff[j++] = ' '; |
443 | } else if (ch == '%') { |
444 | buff[j++] = (byte) Integer.parseInt(encoded.substring(i + 1, i + 3), 16); |
445 | i += 2; |
446 | } else { |
447 | if (SysProperties.CHECK) { |
448 | if (ch > 127 || ch < ' ') { |
449 | throw new IllegalArgumentException( |
450 | "Unexpected char " + (int) ch + " decoding " + encoded); |
451 | } |
452 | } |
453 | buff[j++] = (byte) ch; |
454 | } |
455 | } |
456 | String s = new String(buff, 0, j, Constants.UTF8); |
457 | return s; |
458 | } |
459 | |
460 | /** |
461 | * Split a string into an array of strings using the given separator. A null |
462 | * string will result in a null array, and an empty string in a zero element |
463 | * array. |
464 | * |
465 | * @param s the string to split |
466 | * @param separatorChar the separator character |
467 | * @param trim whether each element should be trimmed |
468 | * @return the array list |
469 | */ |
470 | public static String[] arraySplit(String s, char separatorChar, boolean trim) { |
471 | if (s == null) { |
472 | return null; |
473 | } |
474 | int length = s.length(); |
475 | if (length == 0) { |
476 | return new String[0]; |
477 | } |
478 | ArrayList<String> list = New.arrayList(); |
479 | StringBuilder buff = new StringBuilder(length); |
480 | for (int i = 0; i < length; i++) { |
481 | char c = s.charAt(i); |
482 | if (c == separatorChar) { |
483 | String e = buff.toString(); |
484 | list.add(trim ? e.trim() : e); |
485 | buff.setLength(0); |
486 | } else if (c == '\\' && i < length - 1) { |
487 | buff.append(s.charAt(++i)); |
488 | } else { |
489 | buff.append(c); |
490 | } |
491 | } |
492 | String e = buff.toString(); |
493 | list.add(trim ? e.trim() : e); |
494 | String[] array = new String[list.size()]; |
495 | list.toArray(array); |
496 | return array; |
497 | } |
498 | |
499 | /** |
500 | * Combine an array of strings to one array using the given separator |
501 | * character. A backslash and the separator character and escaped using a |
502 | * backslash. |
503 | * |
504 | * @param list the string array |
505 | * @param separatorChar the separator character |
506 | * @return the combined string |
507 | */ |
508 | public static String arrayCombine(String[] list, char separatorChar) { |
509 | StatementBuilder buff = new StatementBuilder(); |
510 | for (String s : list) { |
511 | buff.appendExceptFirst(String.valueOf(separatorChar)); |
512 | if (s == null) { |
513 | s = ""; |
514 | } |
515 | for (int j = 0, length = s.length(); j < length; j++) { |
516 | char c = s.charAt(j); |
517 | if (c == '\\' || c == separatorChar) { |
518 | buff.append('\\'); |
519 | } |
520 | buff.append(c); |
521 | } |
522 | } |
523 | return buff.toString(); |
524 | } |
525 | |
526 | /** |
527 | * Creates an XML attribute of the form name="value". |
528 | * A single space is prepended to the name, |
529 | * so that multiple attributes can be concatenated. |
530 | * @param name the attribute name |
531 | * @param value the attribute value |
532 | * @return the attribute |
533 | */ |
534 | public static String xmlAttr(String name, String value) { |
535 | return " " + name + "=\"" + xmlText(value) + "\""; |
536 | } |
537 | |
538 | /** |
539 | * Create an XML node with optional attributes and content. |
540 | * The data is indented with 4 spaces if it contains a newline character. |
541 | * |
542 | * @param name the element name |
543 | * @param attributes the attributes (may be null) |
544 | * @param content the content (may be null) |
545 | * @return the node |
546 | */ |
547 | public static String xmlNode(String name, String attributes, String content) { |
548 | return xmlNode(name, attributes, content, true); |
549 | } |
550 | |
551 | /** |
552 | * Create an XML node with optional attributes and content. The data is |
553 | * indented with 4 spaces if it contains a newline character and the indent |
554 | * parameter is set to true. |
555 | * |
556 | * @param name the element name |
557 | * @param attributes the attributes (may be null) |
558 | * @param content the content (may be null) |
559 | * @param indent whether to indent the content if it contains a newline |
560 | * @return the node |
561 | */ |
562 | public static String xmlNode(String name, String attributes, |
563 | String content, boolean indent) { |
564 | String start = attributes == null ? name : name + attributes; |
565 | if (content == null) { |
566 | return "<" + start + "/>\n"; |
567 | } |
568 | if (indent && content.indexOf('\n') >= 0) { |
569 | content = "\n" + indent(content); |
570 | } |
571 | return "<" + start + ">" + content + "</" + name + ">\n"; |
572 | } |
573 | |
574 | /** |
575 | * Indents a string with 4 spaces. |
576 | * |
577 | * @param s the string |
578 | * @return the indented string |
579 | */ |
580 | public static String indent(String s) { |
581 | return indent(s, 4, true); |
582 | } |
583 | |
584 | /** |
585 | * Indents a string with spaces. |
586 | * |
587 | * @param s the string |
588 | * @param spaces the number of spaces |
589 | * @param newline append a newline if there is none |
590 | * @return the indented string |
591 | */ |
592 | public static String indent(String s, int spaces, boolean newline) { |
593 | StringBuilder buff = new StringBuilder(s.length() + spaces); |
594 | for (int i = 0; i < s.length();) { |
595 | for (int j = 0; j < spaces; j++) { |
596 | buff.append(' '); |
597 | } |
598 | int n = s.indexOf('\n', i); |
599 | n = n < 0 ? s.length() : n + 1; |
600 | buff.append(s.substring(i, n)); |
601 | i = n; |
602 | } |
603 | if (newline && !s.endsWith("\n")) { |
604 | buff.append('\n'); |
605 | } |
606 | return buff.toString(); |
607 | } |
608 | |
609 | /** |
610 | * Escapes a comment. |
611 | * If the data contains '--', it is converted to '- -'. |
612 | * The data is indented with 4 spaces if it contains a newline character. |
613 | * |
614 | * @param data the comment text |
615 | * @return <!-- data --> |
616 | */ |
617 | public static String xmlComment(String data) { |
618 | int idx = 0; |
619 | while (true) { |
620 | idx = data.indexOf("--", idx); |
621 | if (idx < 0) { |
622 | break; |
623 | } |
624 | data = data.substring(0, idx + 1) + " " + data.substring(idx + 1); |
625 | } |
626 | // must have a space at the beginning and at the end, |
627 | // otherwise the data must not contain '-' as the first/last character |
628 | if (data.indexOf('\n') >= 0) { |
629 | return "<!--\n" + indent(data) + "-->\n"; |
630 | } |
631 | return "<!-- " + data + " -->\n"; |
632 | } |
633 | |
634 | /** |
635 | * Converts the data to a CDATA element. |
636 | * If the data contains ']]>', it is escaped as a text element. |
637 | * |
638 | * @param data the text data |
639 | * @return <![CDATA[data]]> |
640 | */ |
641 | public static String xmlCData(String data) { |
642 | if (data.contains("]]>")) { |
643 | return xmlText(data); |
644 | } |
645 | boolean newline = data.endsWith("\n"); |
646 | data = "<![CDATA[" + data + "]]>"; |
647 | return newline ? data + "\n" : data; |
648 | } |
649 | |
650 | /** |
651 | * Returns <?xml version="1.0"?> |
652 | * @return <?xml version="1.0"?> |
653 | */ |
654 | public static String xmlStartDoc() { |
655 | return "<?xml version=\"1.0\"?>\n"; |
656 | } |
657 | |
658 | /** |
659 | * Escapes an XML text element. |
660 | * |
661 | * @param text the text data |
662 | * @return the escaped text |
663 | */ |
664 | public static String xmlText(String text) { |
665 | return xmlText(text, false); |
666 | } |
667 | |
668 | /** |
669 | * Escapes an XML text element. |
670 | * |
671 | * @param text the text data |
672 | * @param escapeNewline whether to escape newlines |
673 | * @return the escaped text |
674 | */ |
675 | public static String xmlText(String text, boolean escapeNewline) { |
676 | int length = text.length(); |
677 | StringBuilder buff = new StringBuilder(length); |
678 | for (int i = 0; i < length; i++) { |
679 | char ch = text.charAt(i); |
680 | switch (ch) { |
681 | case '<': |
682 | buff.append("<"); |
683 | break; |
684 | case '>': |
685 | buff.append(">"); |
686 | break; |
687 | case '&': |
688 | buff.append("&"); |
689 | break; |
690 | case '\'': |
691 | buff.append("'"); |
692 | break; |
693 | case '\"': |
694 | buff.append("""); |
695 | break; |
696 | case '\r': |
697 | case '\n': |
698 | if (escapeNewline) { |
699 | buff.append("&#x"). |
700 | append(Integer.toHexString(ch)). |
701 | append(';'); |
702 | } else { |
703 | buff.append(ch); |
704 | } |
705 | break; |
706 | case '\t': |
707 | buff.append(ch); |
708 | break; |
709 | default: |
710 | if (ch < ' ' || ch > 127) { |
711 | buff.append("&#x"). |
712 | append(Integer.toHexString(ch)). |
713 | append(';'); |
714 | } else { |
715 | buff.append(ch); |
716 | } |
717 | } |
718 | } |
719 | return buff.toString(); |
720 | } |
721 | |
722 | /** |
723 | * Replace all occurrences of the before string with the after string. |
724 | * |
725 | * @param s the string |
726 | * @param before the old text |
727 | * @param after the new text |
728 | * @return the string with the before string replaced |
729 | */ |
730 | public static String replaceAll(String s, String before, String after) { |
731 | int next = s.indexOf(before); |
732 | if (next < 0) { |
733 | return s; |
734 | } |
735 | StringBuilder buff = new StringBuilder( |
736 | s.length() - before.length() + after.length()); |
737 | int index = 0; |
738 | while (true) { |
739 | buff.append(s.substring(index, next)).append(after); |
740 | index = next + before.length(); |
741 | next = s.indexOf(before, index); |
742 | if (next < 0) { |
743 | buff.append(s.substring(index)); |
744 | break; |
745 | } |
746 | } |
747 | return buff.toString(); |
748 | } |
749 | |
750 | /** |
751 | * Enclose a string with double quotes. A double quote inside the string is |
752 | * escaped using a double quote. |
753 | * |
754 | * @param s the text |
755 | * @return the double quoted text |
756 | */ |
757 | public static String quoteIdentifier(String s) { |
758 | int length = s.length(); |
759 | StringBuilder buff = new StringBuilder(length + 2); |
760 | buff.append('\"'); |
761 | for (int i = 0; i < length; i++) { |
762 | char c = s.charAt(i); |
763 | if (c == '"') { |
764 | buff.append(c); |
765 | } |
766 | buff.append(c); |
767 | } |
768 | return buff.append('\"').toString(); |
769 | } |
770 | |
771 | /** |
772 | * Check if a String is null or empty (the length is null). |
773 | * |
774 | * @param s the string to check |
775 | * @return true if it is null or empty |
776 | */ |
777 | public static boolean isNullOrEmpty(String s) { |
778 | return s == null || s.length() == 0; |
779 | } |
780 | |
781 | /** |
782 | * In a string, replace block comment marks with /++ .. ++/. |
783 | * |
784 | * @param sql the string |
785 | * @return the resulting string |
786 | */ |
787 | public static String quoteRemarkSQL(String sql) { |
788 | sql = replaceAll(sql, "*/", "++/"); |
789 | return replaceAll(sql, "/*", "/++"); |
790 | } |
791 | |
792 | /** |
793 | * Pad a string. This method is used for the SQL function RPAD and LPAD. |
794 | * |
795 | * @param string the original string |
796 | * @param n the target length |
797 | * @param padding the padding string |
798 | * @param right true if the padding should be appended at the end |
799 | * @return the padded string |
800 | */ |
801 | public static String pad(String string, int n, String padding, boolean right) { |
802 | if (n < 0) { |
803 | n = 0; |
804 | } |
805 | if (n < string.length()) { |
806 | return string.substring(0, n); |
807 | } else if (n == string.length()) { |
808 | return string; |
809 | } |
810 | char paddingChar; |
811 | if (padding == null || padding.length() == 0) { |
812 | paddingChar = ' '; |
813 | } else { |
814 | paddingChar = padding.charAt(0); |
815 | } |
816 | StringBuilder buff = new StringBuilder(n); |
817 | n -= string.length(); |
818 | if (right) { |
819 | buff.append(string); |
820 | } |
821 | for (int i = 0; i < n; i++) { |
822 | buff.append(paddingChar); |
823 | } |
824 | if (!right) { |
825 | buff.append(string); |
826 | } |
827 | return buff.toString(); |
828 | } |
829 | |
830 | /** |
831 | * Create a new char array and copy all the data. If the size of the byte |
832 | * array is zero, the same array is returned. |
833 | * |
834 | * @param chars the char array (may be null) |
835 | * @return a new char array |
836 | */ |
837 | public static char[] cloneCharArray(char[] chars) { |
838 | if (chars == null) { |
839 | return null; |
840 | } |
841 | int len = chars.length; |
842 | if (len == 0) { |
843 | return chars; |
844 | } |
845 | char[] copy = new char[len]; |
846 | System.arraycopy(chars, 0, copy, 0, len); |
847 | return copy; |
848 | } |
849 | |
850 | /** |
851 | * Trim a character from a string. |
852 | * |
853 | * @param s the string |
854 | * @param leading if leading characters should be removed |
855 | * @param trailing if trailing characters should be removed |
856 | * @param sp what to remove (only the first character is used) |
857 | * or null for a space |
858 | * @return the trimmed string |
859 | */ |
860 | public static String trim(String s, boolean leading, boolean trailing, |
861 | String sp) { |
862 | char space = (sp == null || sp.length() < 1) ? ' ' : sp.charAt(0); |
863 | if (leading) { |
864 | int len = s.length(), i = 0; |
865 | while (i < len && s.charAt(i) == space) { |
866 | i++; |
867 | } |
868 | s = (i == 0) ? s : s.substring(i); |
869 | } |
870 | if (trailing) { |
871 | int endIndex = s.length() - 1; |
872 | int i = endIndex; |
873 | while (i >= 0 && s.charAt(i) == space) { |
874 | i--; |
875 | } |
876 | s = i == endIndex ? s : s.substring(0, i + 1); |
877 | } |
878 | return s; |
879 | } |
880 | |
881 | /** |
882 | * Get the string from the cache if possible. If the string has not been |
883 | * found, it is added to the cache. If there is such a string in the cache, |
884 | * that one is returned. |
885 | * |
886 | * @param s the original string |
887 | * @return a string with the same content, if possible from the cache |
888 | */ |
889 | public static String cache(String s) { |
890 | if (!SysProperties.OBJECT_CACHE) { |
891 | return s; |
892 | } |
893 | if (s == null) { |
894 | return s; |
895 | } else if (s.length() == 0) { |
896 | return ""; |
897 | } |
898 | int hash = s.hashCode(); |
899 | String[] cache = getCache(); |
900 | if (cache != null) { |
901 | int index = hash & (SysProperties.OBJECT_CACHE_SIZE - 1); |
902 | String cached = cache[index]; |
903 | if (cached != null) { |
904 | if (s.equals(cached)) { |
905 | return cached; |
906 | } |
907 | } |
908 | cache[index] = s; |
909 | } |
910 | return s; |
911 | } |
912 | |
913 | /** |
914 | * Get a string from the cache, and if no such string has been found, create |
915 | * a new one with only this content. This solves out of memory problems if |
916 | * the string is a substring of another, large string. In Java, strings are |
917 | * shared, which could lead to memory problems. This avoid such problems. |
918 | * |
919 | * @param s the string |
920 | * @return a string that is guaranteed not be a substring of a large string |
921 | */ |
922 | public static String fromCacheOrNew(String s) { |
923 | if (!SysProperties.OBJECT_CACHE) { |
924 | return s; |
925 | } |
926 | if (s == null) { |
927 | return s; |
928 | } else if (s.length() == 0) { |
929 | return ""; |
930 | } |
931 | int hash = s.hashCode(); |
932 | String[] cache = getCache(); |
933 | int index = hash & (SysProperties.OBJECT_CACHE_SIZE - 1); |
934 | if (cache == null) { |
935 | return s; |
936 | } |
937 | String cached = cache[index]; |
938 | if (cached != null) { |
939 | if (s.equals(cached)) { |
940 | return cached; |
941 | } |
942 | } |
943 | // create a new object that is not shared |
944 | // (to avoid out of memory if it is a substring of a big String) |
945 | // (not longer needed for Java 7 update 6 and newer, |
946 | // but the performance overhead is very small for those |
947 | // versions where it is not needed) |
948 | // NOPMD |
949 | s = new String(s); |
950 | cache[index] = s; |
951 | return s; |
952 | } |
953 | |
954 | /** |
955 | * Clear the cache. This method is used for testing. |
956 | */ |
957 | public static void clearCache() { |
958 | softCache = new SoftReference<String[]>(null); |
959 | } |
960 | |
961 | /** |
962 | * Convert a hex encoded string to a byte array. |
963 | * |
964 | * @param s the hex encoded string |
965 | * @return the byte array |
966 | */ |
967 | public static byte[] convertHexToBytes(String s) { |
968 | int len = s.length(); |
969 | if (len % 2 != 0) { |
970 | throw DbException.get(ErrorCode.HEX_STRING_ODD_1, s); |
971 | } |
972 | len /= 2; |
973 | byte[] buff = new byte[len]; |
974 | int mask = 0; |
975 | int[] hex = HEX_DECODE; |
976 | try { |
977 | for (int i = 0; i < len; i++) { |
978 | int d = hex[s.charAt(i + i)] << 4 | hex[s.charAt(i + i + 1)]; |
979 | mask |= d; |
980 | buff[i] = (byte) d; |
981 | } |
982 | } catch (ArrayIndexOutOfBoundsException e) { |
983 | throw DbException.get(ErrorCode.HEX_STRING_WRONG_1, s); |
984 | } |
985 | if ((mask & ~255) != 0) { |
986 | throw DbException.get(ErrorCode.HEX_STRING_WRONG_1, s); |
987 | } |
988 | return buff; |
989 | } |
990 | |
991 | /** |
992 | * Convert a byte array to a hex encoded string. |
993 | * |
994 | * @param value the byte array |
995 | * @return the hex encoded string |
996 | */ |
997 | public static String convertBytesToHex(byte[] value) { |
998 | return convertBytesToHex(value, value.length); |
999 | } |
1000 | |
1001 | /** |
1002 | * Convert a byte array to a hex encoded string. |
1003 | * |
1004 | * @param value the byte array |
1005 | * @param len the number of bytes to encode |
1006 | * @return the hex encoded string |
1007 | */ |
1008 | public static String convertBytesToHex(byte[] value, int len) { |
1009 | char[] buff = new char[len + len]; |
1010 | char[] hex = HEX; |
1011 | for (int i = 0; i < len; i++) { |
1012 | int c = value[i] & 0xff; |
1013 | buff[i + i] = hex[c >> 4]; |
1014 | buff[i + i + 1] = hex[c & 0xf]; |
1015 | } |
1016 | return new String(buff); |
1017 | } |
1018 | |
1019 | /** |
1020 | * Check if this string is a decimal number. |
1021 | * |
1022 | * @param s the string |
1023 | * @return true if it is |
1024 | */ |
1025 | public static boolean isNumber(String s) { |
1026 | if (s.length() == 0) { |
1027 | return false; |
1028 | } |
1029 | for (char c : s.toCharArray()) { |
1030 | if (!Character.isDigit(c)) { |
1031 | return false; |
1032 | } |
1033 | } |
1034 | return true; |
1035 | } |
1036 | |
1037 | /** |
1038 | * Append a zero-padded number to a string builder. |
1039 | * |
1040 | * @param buff the string builder |
1041 | * @param length the number of characters to append |
1042 | * @param positiveValue the number to append |
1043 | */ |
1044 | public static void appendZeroPadded(StringBuilder buff, int length, |
1045 | long positiveValue) { |
1046 | if (length == 2) { |
1047 | if (positiveValue < 10) { |
1048 | buff.append('0'); |
1049 | } |
1050 | buff.append(positiveValue); |
1051 | } else { |
1052 | String s = Long.toString(positiveValue); |
1053 | length -= s.length(); |
1054 | while (length > 0) { |
1055 | buff.append('0'); |
1056 | length--; |
1057 | } |
1058 | buff.append(s); |
1059 | } |
1060 | } |
1061 | |
1062 | /** |
1063 | * Escape table or schema patterns used for DatabaseMetaData functions. |
1064 | * |
1065 | * @param pattern the pattern |
1066 | * @return the escaped pattern |
1067 | */ |
1068 | public static String escapeMetaDataPattern(String pattern) { |
1069 | if (pattern == null || pattern.length() == 0) { |
1070 | return pattern; |
1071 | } |
1072 | return replaceAll(pattern, "\\", "\\\\"); |
1073 | } |
1074 | |
1075 | } |