1 | // Jomic - a viewer for comic book archives. |
2 | // Copyright (C) 2004-2011 Thomas Aglassinger |
3 | // |
4 | // This program is free software: you can redistribute it and/or modify |
5 | // it under the terms of the GNU General Public License as published by |
6 | // the Free Software Foundation, either version 3 of the License, or |
7 | // (at your option) any later version. |
8 | // |
9 | // This program is distributed in the hope that it will be useful, |
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | // GNU General Public License for more details. |
13 | // |
14 | // You should have received a copy of the GNU General Public License |
15 | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | package net.sf.jomic.tools; |
17 | |
18 | import java.awt.Color; |
19 | import java.io.EOFException; |
20 | import java.io.File; |
21 | import java.io.IOException; |
22 | import java.text.CharacterIterator; |
23 | import java.text.DecimalFormat; |
24 | import java.text.NumberFormat; |
25 | import java.text.ParseException; |
26 | import java.text.StringCharacterIterator; |
27 | import java.util.Arrays; |
28 | import java.util.Iterator; |
29 | import java.util.LinkedList; |
30 | import java.util.List; |
31 | import java.util.Map; |
32 | import java.util.StringTokenizer; |
33 | import java.util.TreeMap; |
34 | |
35 | import org.apache.commons.logging.Log; |
36 | import org.apache.commons.logging.LogFactory; |
37 | |
38 | /** |
39 | * Utility class to simplify dealing with Strings. |
40 | * |
41 | * @author Thomas Aglassinger |
42 | */ |
43 | public final class StringTools |
44 | { |
45 | private static final int ASCII_CODE_OF_SPACE = 32; |
46 | private static final char BEFORE_COMMA_OR_CLOSE_BRAKET = ','; |
47 | private static final char BEFORE_END = 'x'; |
48 | private static final char BEFORE_OPEN_BRAKET = '['; |
49 | private static final char BEFORE_VALUE_OR_CLOSE_BRAKET = ']'; |
50 | private static final String CLOSE_BRAKET = "]"; |
51 | private static final int COLOR_HEX_DIGIT_COUNT = 6; |
52 | /** |
53 | * Size of slack in buffer escaped characters get encoded to. |
54 | */ |
55 | private static final int ESCAPE_SLACK_COUNT = 16; |
56 | private static final int LAST_ASCII_CODE = 127; |
57 | private static final double LOG_OF_10 = Math.log(10); |
58 | private static final String OPEN_BRAKET = "["; |
59 | private static final int RGB_MASK = 0xffffff; |
60 | private static final int UNICODE_HEX_DIGIT_COUNT = 4; |
61 | |
62 | /** |
63 | * Minimum RGB value. This is < 0 because negative values represent 2-complements of values |
64 | * with a high red component. |
65 | */ |
66 | //@ private static ghost final int RGB_MIN_VALUE = -0x800000; |
67 | //@ private static ghost final int MAX_HEX_DIGIT_COUNT = 16; |
68 | |
69 | private static /*@ spec_public nullable @*/ StringTools instance; |
70 | private ErrorTools errorTools; |
71 | private Log logger; |
72 | private NumberFormat percentFormat; |
73 | private Map escapeMap; |
74 | |
75 | private StringTools() { |
76 | logger = LogFactory.getLog(StringTools.class); |
77 | errorTools = ErrorTools.instance(); |
78 | percentFormat = new DecimalFormat("#.00"); |
79 | escapeMap = new TreeMap(); |
80 | escapeMap.put(new Character('\"'), "\\\""); |
81 | escapeMap.put(new Character('\''), "\\\'"); |
82 | escapeMap.put(new Character('\\'), "\\\\"); |
83 | escapeMap.put(new Character('\b'), "\\b"); |
84 | escapeMap.put(new Character('\f'), "\\f"); |
85 | escapeMap.put(new Character('\n'), "\\n"); |
86 | escapeMap.put(new Character('\r'), "\\r"); |
87 | escapeMap.put(new Character('\t'), "\\t"); |
88 | } |
89 | |
90 | /** |
91 | * Get number of digits required to represent <code>value</code> (including a possible '-' for |
92 | * negative numbers). |
93 | */ |
94 | //@ ensures \result > 0; |
95 | //@ ensures (value < 0) ==> (\result > 0); |
96 | public /*@ pure @*/ int getDigitCount(int value) { |
97 | int result; |
98 | |
99 | if (value == 0) { |
100 | result = 1; |
101 | } else { |
102 | int relevantValue; |
103 | |
104 | if (value == Integer.MIN_VALUE) { |
105 | // Take into account that Math.abs(Integer.MIN_VALUE) still yields Integer.MIN_VALUE. |
106 | relevantValue = Integer.MAX_VALUE - 1; |
107 | } else { |
108 | // Take into account that (Integer.MAX_VALUE + 1) yields 0. |
109 | relevantValue = Math.min(Math.abs(value), Integer.MAX_VALUE - 1); |
110 | } |
111 | |
112 | result = (int) Math.ceil(log10((relevantValue + 1))); |
113 | if (value < 0) { |
114 | // Add another digit got the "-" sign. |
115 | result += 1; |
116 | } |
117 | } |
118 | return result; |
119 | } |
120 | |
121 | /** |
122 | * Get DecimalFormat with leading zeroes that can handle up to <code>maxValue</code>. |
123 | */ |
124 | //@ requires maxValue >= 0; |
125 | public /*@ pure @*/ DecimalFormat getLeadingZeroFormat(int maxValue) { |
126 | int digitCount = getDigitCount(maxValue); |
127 | String pattern = "0000000000".substring(0, digitCount); |
128 | DecimalFormat result = new DecimalFormat(pattern); |
129 | |
130 | return result; |
131 | } |
132 | |
133 | /** |
134 | * Get unique mnemonics for all <code>labels</code>. |
135 | * |
136 | * @param labels the long texts that need mnemonics |
137 | * @param suggestions an array with the same length as <code>labels</code> that can already |
138 | * contain mnemonics for certain labels. For the rest, use <code>'\0'</code> as suggestion |
139 | * so the will be computed automatically. A suggestion of <code>null</code> means that all |
140 | * mnemonics should be computed. |
141 | */ |
142 | //@ requires labels.length > 0; |
143 | //@ requires (suggestions != null) ==> (suggestions.length == labels.length); |
144 | //@ ensures \result.length == labels.length; |
145 | public char[] getMnemonics(String[] labels, /*@ nullable @*/ char[] suggestions) { |
146 | int mnemonicCount = labels.length; |
147 | char[] result = new char[mnemonicCount]; |
148 | |
149 | if (suggestions != null) { |
150 | int suggestionCount = suggestions.length; |
151 | |
152 | for (int i = 0; i < suggestionCount; i += 1) { |
153 | result[i] = suggestions[i]; |
154 | } |
155 | } |
156 | |
157 | for (int i = 0; i < mnemonicCount; i += 1) { |
158 | String label = labels[i]; |
159 | char mnemonic = 0; |
160 | |
161 | // Check that we have some letters. |
162 | int labelLength = label.length(); |
163 | int j = 0; |
164 | |
165 | while ((j < labelLength) && !isVisibleChar(label.charAt(j))) { |
166 | j += 1; |
167 | } |
168 | //@ assert j < labelLength; |
169 | |
170 | // Pass 1: try first letters of words |
171 | StringTokenizer tokenizer = new StringTokenizer(label); |
172 | |
173 | while (tokenizer.hasMoreTokens() && (mnemonic == 0)) { |
174 | String token = tokenizer.nextToken(); |
175 | |
176 | //@ assert token.length() > 0; |
177 | char candidate = Character.toLowerCase(getNextVisibleChar(token, 0)); |
178 | |
179 | if (candidate != 0) { |
180 | if (!hasChar(result, candidate)) { |
181 | mnemonic = candidate; |
182 | if (logger.isDebugEnabled()) { |
183 | logger.debug("found mnemonic for " + sourced(label) + ": " |
184 | + mnemonic + " (looking for words)"); |
185 | } |
186 | } |
187 | } |
188 | } |
189 | |
190 | // Pass 2: try any letter |
191 | if (mnemonic == 0) { |
192 | int k = 0; |
193 | |
194 | while ((k < labelLength) && (mnemonic == 0)) { |
195 | char candidate = Character.toLowerCase(label.charAt(k)); |
196 | |
197 | if (isVisibleChar(candidate) && !hasChar(result, candidate)) { |
198 | mnemonic = candidate; |
199 | if (logger.isDebugEnabled()) { |
200 | logger.debug("found mnemonic for " + sourced(label) + ": " |
201 | + mnemonic + " (looking for any letter)"); |
202 | } |
203 | } else { |
204 | k += 1; |
205 | } |
206 | } |
207 | } |
208 | |
209 | if (mnemonic == 0) { |
210 | // Give up, the current algorithm is too dumb to find anything |
211 | // TODO: make auto-mnemonics work in special, nasty cases |
212 | throw new IllegalStateException("cannot find mnemonic for label " + i + ": " |
213 | + sourced(label)); |
214 | } |
215 | |
216 | //@ assert mnemonic == Character.toLowerCase(mnemonic); |
217 | result[i] = mnemonic; |
218 | } |
219 | |
220 | return result; |
221 | } |
222 | |
223 | /** |
224 | * Get <code>value</code> formatted as percent. |
225 | */ |
226 | public /*@ pure @*/ String getPercentText(double value) { |
227 | return percentFormat.format(value); |
228 | } |
229 | |
230 | /** |
231 | * Is <code>some</code> null or only consisting of white space? |
232 | */ |
233 | public /*@ pure @*/ boolean isNullOrEmpty(/*@ nullable @*/ String some) { |
234 | boolean result; |
235 | |
236 | if (some == null) { |
237 | result = true; |
238 | } else { |
239 | result = (some.trim().length() == 0); |
240 | } |
241 | return result; |
242 | } |
243 | |
244 | /** |
245 | * Return <code>true</code> if <code>some</code> is sorted. |
246 | * |
247 | * @see String#compareTo(java.lang.String) |
248 | */ |
249 | public /*@ pure @*/ boolean isSorted(String[] some) { |
250 | boolean result = true; |
251 | int i = 0; |
252 | |
253 | while (result && (i < some.length - 1)) { |
254 | if (some[i].compareTo(some[i + 1]) > 0) { |
255 | result = false; |
256 | } |
257 | } |
258 | return result; |
259 | } |
260 | |
261 | /** |
262 | * Are all characters in <code>some</code> white space? If <code>some</code> is empty, return |
263 | * <code>false</code>. |
264 | */ |
265 | public /*@ pure @*/ boolean isWhiteSpace(String some) { |
266 | boolean result = some.length() > 0; |
267 | int index = 0; |
268 | |
269 | while (result && (index < some.length())) { |
270 | if (Character.isWhitespace(some.charAt(index))) { |
271 | index += 1; |
272 | } else { |
273 | result = false; |
274 | } |
275 | } |
276 | return result; |
277 | } |
278 | |
279 | private /*@ pure @*/ char getNextVisibleChar(String text, int startIndex) { |
280 | char result = 0; |
281 | int index = startIndex; |
282 | |
283 | while (index < text.length() && (result == 0)) { |
284 | char nextChar = text.charAt(index); |
285 | |
286 | if (isVisibleChar(nextChar)) { |
287 | result = nextChar; |
288 | } else { |
289 | index += 1; |
290 | } |
291 | } |
292 | return result; |
293 | } |
294 | |
295 | /** |
296 | * Returns true if <code>some</code> is visible. |
297 | */ |
298 | private /*@ pure @*/ boolean isVisibleChar(char some) { |
299 | return some > ASCII_CODE_OF_SPACE; |
300 | } |
301 | |
302 | /** |
303 | * Get accessor to unique instance. |
304 | */ |
305 | //@ assignable instance; |
306 | public static synchronized StringTools instance() { |
307 | if (instance == null) { |
308 | instance = new StringTools(); |
309 | } |
310 | return instance; |
311 | } |
312 | |
313 | public /*@ pure @*/ String arrayToString(double[] data) { |
314 | String result = "["; |
315 | |
316 | for (int i = 0; i < data.length; i += 1) { |
317 | if (i > 0) { |
318 | result += ", "; |
319 | } |
320 | result += data[i]; |
321 | } |
322 | result += "]"; |
323 | return result; |
324 | } |
325 | |
326 | public /*@ pure @*/ String arrayToString(File[] data) { |
327 | String result = "["; |
328 | |
329 | for (int i = 0; i < data.length; i += 1) { |
330 | if (i > 0) { |
331 | result += ", "; |
332 | } |
333 | result += sourced(data[i]); |
334 | } |
335 | result += "]"; |
336 | return result; |
337 | } |
338 | |
339 | public /*@ pure @*/ String arrayToString(float[] data) { |
340 | String result = "["; |
341 | |
342 | for (int i = 0; i < data.length; i += 1) { |
343 | if (i > 0) { |
344 | result += ", "; |
345 | } |
346 | result += data[i]; |
347 | } |
348 | result += "]"; |
349 | return result; |
350 | } |
351 | |
352 | public /*@ pure @*/ String arrayToString(int[] data) { |
353 | String result = "["; |
354 | |
355 | for (int i = 0; i < data.length; i += 1) { |
356 | if (i > 0) { |
357 | result += ", "; |
358 | } |
359 | result += data[i]; |
360 | } |
361 | result += "]"; |
362 | return result; |
363 | } |
364 | |
365 | public /*@ pure @*/ String arrayToString(String[] data) { |
366 | String result = "["; |
367 | |
368 | for (int i = 0; i < data.length; i += 1) { |
369 | if (i > 0) { |
370 | result += ", "; |
371 | } |
372 | result += sourced(data[i]); |
373 | } |
374 | result += "]"; |
375 | return result; |
376 | } |
377 | |
378 | /** |
379 | * Escaped as HTML attribute value. For example, "&" becomes "&amp;". |
380 | */ |
381 | public /*@ pure @*/ String asHtmlEscaped(String some) { |
382 | StringBuffer result = new StringBuffer(); |
383 | StringCharacterIterator iterator = new StringCharacterIterator(some); |
384 | char character = iterator.current(); |
385 | |
386 | while (character != CharacterIterator.DONE) { |
387 | if (character == '<') { |
388 | result.append("<"); |
389 | } else if (character == '>') { |
390 | result.append(">"); |
391 | } else if (character == '\"') { |
392 | result.append("""); |
393 | } else if (character == '\'') { |
394 | result.append("'"); |
395 | } else if (character == '&') { |
396 | result.append("&"); |
397 | } else if ((character < ASCII_CODE_OF_SPACE) || (character > LAST_ASCII_CODE)) { |
398 | result.append("&#" + (int) character + ";"); |
399 | } else { |
400 | result.append(character); |
401 | } |
402 | character = iterator.next(); |
403 | } |
404 | return result.toString(); |
405 | } |
406 | |
407 | /** |
408 | * Return <code>some</code> converted to "camel case" using "-" or "_" as word seperators. For |
409 | * example, "some-stuff-to-do" yields "someStuffToDo". |
410 | */ |
411 | //@ ensures \result.length() <= some.length(); |
412 | public /*@ pure @*/ String camelized(String some) { |
413 | StringBuffer result = new StringBuffer(); |
414 | boolean upperNext = false; |
415 | |
416 | for (int i = 0; i < some.length(); i += 1) { |
417 | char c = some.charAt(i); |
418 | |
419 | if ((c == '-') || (c == '_')) { |
420 | upperNext = true; |
421 | } else { |
422 | if (upperNext) { |
423 | c = Character.toUpperCase(c); |
424 | upperNext = false; |
425 | } |
426 | result.append(c); |
427 | } |
428 | } |
429 | |
430 | return result.toString(); |
431 | } |
432 | |
433 | /** |
434 | * Hex-color representation of RGB color value. For example, and RGB tupple of red=0x12, |
435 | * green=0x34, blue=0x56 would be encoded into an <code>int</code> of 0x123456, and yield |
436 | * "#123456". (This is the same principle used to represent colors in HTML.) |
437 | * |
438 | * @see Color#getRGB() |
439 | */ |
440 | //@ ensures \result.length() == 7; |
441 | //@ ensures \result.charAt(0) == '#'; |
442 | //@ ensures RGB_MIN_VALUE <= (RGB_MASK & rgba); |
443 | public /*@ pure @*/ String colorString(long rgba) { |
444 | long rgb = RGB_MASK & rgba; |
445 | String result = hexString(rgb, COLOR_HEX_DIGIT_COUNT, ""); |
446 | |
447 | |
448 | result = "#" + result.substring(result.length() - COLOR_HEX_DIGIT_COUNT); |
449 | return result; |
450 | } |
451 | |
452 | /** |
453 | * Hex-color representation of <code>color</code>. |
454 | * |
455 | * @see java.awt.Color#getRGB() |
456 | */ |
457 | //@ ensures \result.length() == 7; |
458 | //@ ensures \result.charAt(0) == '#'; |
459 | //@ ensures RGB_MIN_VALUE <= (RGB_MASK & rgba); |
460 | public /*@ pure @*/ String colorString(Color color) { |
461 | return colorString(color.getRGB()); |
462 | } |
463 | |
464 | //@ requires \nonnullelements(haystack); |
465 | //@ requires isSorted(haystack); |
466 | public boolean equalsAnyOf(String[] haystack, String needle) { |
467 | return equalsAnyOf(haystack, needle, false); |
468 | } |
469 | |
470 | //@ requires \nonnullelements(haystack); |
471 | // TODO: assert !sort implies "sorted" |
472 | // TODO: Change "sort" to "sorted"; if true, do a binary search, otherwise a sequential search. |
473 | public boolean equalsAnyOf(String[] haystack, String needle, boolean sort) { |
474 | boolean result; |
475 | String[] sortedHaystack; |
476 | |
477 | if (sort) { |
478 | int haystackLength = haystack.length; |
479 | |
480 | sortedHaystack = new String[haystackLength]; |
481 | System.arraycopy(haystack, 0, sortedHaystack, 0, haystackLength); |
482 | Arrays.sort(sortedHaystack); |
483 | } else { |
484 | sortedHaystack = haystack; |
485 | } |
486 | result = (Arrays.binarySearch(sortedHaystack, needle) >= 0); |
487 | return result; |
488 | } |
489 | |
490 | /** |
491 | * Extract number by moving backwards starting at <code>endIndex</code>. If the character at |
492 | * <code>endIndex</code> is no digit, return an empty string (""). |
493 | */ |
494 | //@ requires endIndex >= 0; |
495 | //@ requires endIndex < possiblyPagedName.length(); |
496 | public String extractNumberBackwards(String possiblyPagedName, int endIndex) { |
497 | String result; |
498 | int index = endIndex; |
499 | |
500 | while ((index >= 0) && Character.isDigit(possiblyPagedName.charAt(index))) { |
501 | index -= 1; |
502 | } |
503 | result = possiblyPagedName.substring(index + 1, endIndex + 1); |
504 | |
505 | return result; |
506 | } |
507 | |
508 | /** |
509 | * Hex representation of <code>value</code>. |
510 | * |
511 | * @param digits the number of digits the result should have at least; if necessary, leading |
512 | * "0"s are prepended |
513 | * @param prefix the text to be used as the fist few characters of the result; "0x" if null. |
514 | */ |
515 | //@ requires digits > 0; |
516 | //@ requires digits <= MAX_HEX_DIGIT_COUNT; |
517 | public /*@ pure @*/ String hexString(long value, int digits, /*@ nullable @*/ String prefix) { |
518 | String result = Long.toHexString(value); |
519 | String actualPrefix; |
520 | |
521 | if (prefix == null) { |
522 | actualPrefix = "0x"; |
523 | } else { |
524 | actualPrefix = prefix; |
525 | } |
526 | |
527 | if (result.length() < digits) { |
528 | String zeros = "000000000000000"; |
529 | |
530 | //@ assert zeros.length() == (MAX_HEX_DIGIT_COUNT - 1); |
531 | result = zeros.substring(result.length() - 1, digits - 1) + result; |
532 | } |
533 | result = actualPrefix + result; |
534 | return result; |
535 | } |
536 | |
537 | |
538 | /** |
539 | * Hex representation of <code>value</code>, prefixed with "0x". |
540 | * |
541 | * @param digits the number of digits the result should have at least; if necessary, leading |
542 | * "0"s are prepended |
543 | */ |
544 | //@ requires digits > 0; |
545 | //@ ensures \result.length() >= (2 + digits); |
546 | public /*@ pure @*/ String hexString(long value, int digits) { |
547 | return hexString(value, digits, null); |
548 | } |
549 | |
550 | /** |
551 | * Get 10-log. |
552 | */ |
553 | //@ requires some > 0; |
554 | public /*@ pure @*/ double log10(double some) { |
555 | return Math.log(some) / LOG_OF_10; |
556 | } |
557 | |
558 | private boolean isEscapable(Character some) { |
559 | return escapeMap.containsKey(some); |
560 | } |
561 | |
562 | /** |
563 | * Source code version of <code>some</code> that can be pasted into a Java source. The result |
564 | * is embedded in two quotes, escape characters are rendered where possible. Invisible |
565 | * characters are rendered as unicode escape. The value <code>null</code> results in the the |
566 | * text "null" (without quotes). |
567 | */ |
568 | public /*@ pure @*/ String sourced(/*@ nullable @*/ String some) { |
569 | String result; |
570 | |
571 | if (some == null) { |
572 | result = "null"; |
573 | } else { |
574 | StringBuffer buffer = new StringBuffer(some.length() + ESCAPE_SLACK_COUNT); |
575 | |
576 | buffer.append('\"'); |
577 | for (int i = 0; i < some.length(); i += 1) { |
578 | char c = some.charAt(i); |
579 | Character cAsCharacter = new Character(c); |
580 | String escaped = null; |
581 | |
582 | if (isEscapable(cAsCharacter)) { |
583 | escaped = (String) escapeMap.get(cAsCharacter); |
584 | } else if (c < ' ') { |
585 | escaped = hexString(c, UNICODE_HEX_DIGIT_COUNT, "\\u"); |
586 | } |
587 | if (escaped == null) { |
588 | buffer.append(c); |
589 | } else { |
590 | if (logger.isDebugEnabled()) { |
591 | logger.debug("escaping: " + c + " -> " + escaped); |
592 | } |
593 | buffer.append(escaped); |
594 | } |
595 | } |
596 | buffer.append('\"'); |
597 | result = buffer.toString(); |
598 | } |
599 | return result; |
600 | } |
601 | |
602 | /** |
603 | * Source code version of character <code>some</code> that can be pasted into a Java source. |
604 | * The result is embedded in two quotes, escape characters are rendered where possible. |
605 | * Invisible characters are rendered as unicode escape. |
606 | */ |
607 | public /*@ pure @*/ String sourced(char some) { |
608 | return sourced(Character.toString(some)); |
609 | } |
610 | |
611 | /** |
612 | * Same as <code>sourced(String)</code>, but uses absolute file path. |
613 | * |
614 | * @see #sourced(String) |
615 | */ |
616 | public /*@ pure @*/ String sourced(/*@ nullable @*/ File some) { |
617 | String result; |
618 | |
619 | if (some == null) { |
620 | result = sourced((String) null); |
621 | } else { |
622 | result = sourced(some.getAbsolutePath()); |
623 | } |
624 | return result; |
625 | } |
626 | |
627 | public int[] stringToIntArray(String data) |
628 | throws IOException, ParseException { |
629 | int[] result; |
630 | char parserState = BEFORE_OPEN_BRAKET; |
631 | List values = new LinkedList(); |
632 | StringTokenizer parser = new StringTokenizer(data, "," + OPEN_BRAKET + CLOSE_BRAKET, true); |
633 | LocaleTools localeTools = LocaleTools.instance(); |
634 | |
635 | while (parserState != BEFORE_END) { |
636 | if (parser.hasMoreTokens()) { |
637 | // Get next token without white space |
638 | String token = parser.nextToken().trim(); |
639 | |
640 | if (logger.isDebugEnabled()) { |
641 | logger.debug("parse: state=" + parserState + ", token=" + sourced(token)); |
642 | } |
643 | if (token.length() != 0) { |
644 | if (parserState == BEFORE_OPEN_BRAKET) { |
645 | if (token.equals(OPEN_BRAKET)) { |
646 | parserState = BEFORE_VALUE_OR_CLOSE_BRAKET; |
647 | } else { |
648 | String message = localeTools.getMessage("errors.arrayMustStartWithOpenBraket", |
649 | new Object[]{sourced(OPEN_BRAKET), sourced(token)}); |
650 | |
651 | throw new ParseException(message, 0); |
652 | } |
653 | } else if (parserState == BEFORE_VALUE_OR_CLOSE_BRAKET) { |
654 | if (token.equals(CLOSE_BRAKET)) { |
655 | parserState = BEFORE_END; |
656 | } else { |
657 | Integer integerValue = new Integer(token); |
658 | |
659 | values.add(integerValue); |
660 | parserState = BEFORE_COMMA_OR_CLOSE_BRAKET; |
661 | } |
662 | } else if (parserState == BEFORE_COMMA_OR_CLOSE_BRAKET) { |
663 | if (token.equals(CLOSE_BRAKET)) { |
664 | parserState = BEFORE_END; |
665 | } else if (token.equals(",")) { |
666 | parserState = BEFORE_VALUE_OR_CLOSE_BRAKET; |
667 | } else { |
668 | String message = localeTools.getMessage("errors.afterValueCommaOrBraketMustFollow"); |
669 | |
670 | throw new ParseException(message, 0); |
671 | } |
672 | } else { |
673 | //@ assert false; |
674 | } |
675 | } |
676 | } else { |
677 | throw new EOFException(); |
678 | } |
679 | } |
680 | |
681 | result = integerListToArray(values); |
682 | return result; |
683 | } |
684 | |
685 | /** |
686 | * Some as <code>some</code>, but with the first character converted to title case. |
687 | * |
688 | * @see Character#toTitleCase(char) |
689 | */ |
690 | //@ ensures (some.length() > 1) ==> some.substring(1).equals(\result.substring(1)); |
691 | public /*@ pure @*/ String titled(String some) { |
692 | return errorTools.titled(some); |
693 | } |
694 | |
695 | /** |
696 | * If <code>text</code> starts with <code>prefix</code>, trim it from the left, otherwise just |
697 | * return <code>text</code>. |
698 | */ |
699 | public /*@ pure @*/ String trimPrefix(String text, String prefix) { |
700 | String result; |
701 | |
702 | if (text.startsWith(prefix)) { |
703 | result = text.substring(prefix.length()); |
704 | } else { |
705 | result = text; |
706 | } |
707 | return result; |
708 | } |
709 | |
710 | /** |
711 | * Return <code>true</code> if <code>haystack</code> contains <code>needle</code>. |
712 | */ |
713 | private /*@ pure @*/ boolean hasChar(char[] haystack, char needle) { |
714 | boolean result = false; |
715 | int i = 0; |
716 | |
717 | while (!result && i < haystack.length) { |
718 | if (haystack[i] == needle) { |
719 | result = true; |
720 | } else { |
721 | i += 1; |
722 | } |
723 | } |
724 | return result; |
725 | } |
726 | |
727 | //@ requires \nonnullelements(list.toArray()); |
728 | //@ requires (\forall int i; (i >= 0) && (i < list.size()); list.get(i) instanceof Integer); |
729 | //@ ensures \result.length == list.size(); |
730 | //@ ensures (\forall int i; (i >= 0) && (i < list.size()); \result[i] == ((Integer) list.get(i)).intValue()); |
731 | private /*@ pure @*/ int[] integerListToArray(List list) { |
732 | int[] result; |
733 | |
734 | synchronized (list) { |
735 | int itemCount = list.size(); |
736 | |
737 | result = new int[itemCount]; |
738 | |
739 | int index = 0; |
740 | Iterator rider = list.iterator(); |
741 | |
742 | while (rider.hasNext()) { |
743 | Integer value = (Integer) rider.next(); |
744 | |
745 | result[index] = value.intValue(); |
746 | index += 1; |
747 | } |
748 | //@ assert itemCount == index; |
749 | } |
750 | return result; |
751 | } |
752 | } |