View Javadoc
1   /*
2    * Copyright (C) 2009-2010, Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.util;
12  
13  import java.text.MessageFormat;
14  import java.util.Collection;
15  
16  import org.eclipse.jgit.annotations.NonNull;
17  import org.eclipse.jgit.internal.JGitText;
18  
19  /**
20   * Miscellaneous string comparison utility methods.
21   */
22  public final class StringUtils {
23  
24  	private static final long KiB = 1024;
25  
26  	private static final long MiB = 1024 * KiB;
27  
28  	private static final long GiB = 1024 * MiB;
29  
30  	private static final char[] LC;
31  
32  	static {
33  		LC = new char['Z' + 1];
34  		for (char c = 0; c < LC.length; c++)
35  			LC[c] = c;
36  		for (char c = 'A'; c <= 'Z'; c++)
37  			LC[c] = (char) ('a' + (c - 'A'));
38  	}
39  
40  	/**
41  	 * Convert the input to lowercase.
42  	 * <p>
43  	 * This method does not honor the JVM locale, but instead always behaves as
44  	 * though it is in the US-ASCII locale. Only characters in the range 'A'
45  	 * through 'Z' are converted. All other characters are left as-is, even if
46  	 * they otherwise would have a lowercase character equivalent.
47  	 *
48  	 * @param c
49  	 *            the input character.
50  	 * @return lowercase version of the input.
51  	 */
52  	public static char toLowerCase(char c) {
53  		return c <= 'Z' ? LC[c] : c;
54  	}
55  
56  	/**
57  	 * Convert the input string to lower case, according to the "C" locale.
58  	 * <p>
59  	 * This method does not honor the JVM locale, but instead always behaves as
60  	 * though it is in the US-ASCII locale. Only characters in the range 'A'
61  	 * through 'Z' are converted, all other characters are left as-is, even if
62  	 * they otherwise would have a lowercase character equivalent.
63  	 *
64  	 * @param in
65  	 *            the input string. Must not be null.
66  	 * @return a copy of the input string, after converting characters in the
67  	 *         range 'A'..'Z' to 'a'..'z'.
68  	 */
69  	public static String toLowerCase(String in) {
70  		final StringBuilder r = new StringBuilder(in.length());
71  		for (int i = 0; i < in.length(); i++)
72  			r.append(toLowerCase(in.charAt(i)));
73  		return r.toString();
74  	}
75  
76  
77  	/**
78  	 * Borrowed from commons-lang <code>StringUtils.capitalize()</code> method.
79  	 *
80  	 * <p>
81  	 * Capitalizes a String changing the first letter to title case as per
82  	 * {@link java.lang.Character#toTitleCase(char)}. No other letters are
83  	 * changed.
84  	 * </p>
85  	 * <p>
86  	 * A <code>null</code> input String returns <code>null</code>.
87  	 * </p>
88  	 *
89  	 * @param str
90  	 *            the String to capitalize, may be null
91  	 * @return the capitalized String, <code>null</code> if null String input
92  	 * @since 4.0
93  	 */
94  	public static String capitalize(String str) {
95  		int strLen;
96  		if (str == null || (strLen = str.length()) == 0) {
97  			return str;
98  		}
99  		return new StringBuilder(strLen)
100 				.append(Character.toTitleCase(str.charAt(0)))
101 				.append(str.substring(1)).toString();
102 	}
103 
104 	/**
105 	 * Test if two strings are equal, ignoring case.
106 	 * <p>
107 	 * This method does not honor the JVM locale, but instead always behaves as
108 	 * though it is in the US-ASCII locale.
109 	 *
110 	 * @param a
111 	 *            first string to compare.
112 	 * @param b
113 	 *            second string to compare.
114 	 * @return true if a equals b
115 	 */
116 	public static boolean equalsIgnoreCase(String a, String b) {
117 		if (References.isSameObject(a, b)) {
118 			return true;
119 		}
120 		if (a.length() != b.length())
121 			return false;
122 		for (int i = 0; i < a.length(); i++) {
123 			if (toLowerCase(a.charAt(i)) != toLowerCase(b.charAt(i)))
124 				return false;
125 		}
126 		return true;
127 	}
128 
129 	/**
130 	 * Compare two strings, ignoring case.
131 	 * <p>
132 	 * This method does not honor the JVM locale, but instead always behaves as
133 	 * though it is in the US-ASCII locale.
134 	 *
135 	 * @param a
136 	 *            first string to compare.
137 	 * @param b
138 	 *            second string to compare.
139 	 * @since 2.0
140 	 * @return an int.
141 	 */
142 	public static int compareIgnoreCase(String a, String b) {
143 		for (int i = 0; i < a.length() && i < b.length(); i++) {
144 			int d = toLowerCase(a.charAt(i)) - toLowerCase(b.charAt(i));
145 			if (d != 0)
146 				return d;
147 		}
148 		return a.length() - b.length();
149 	}
150 
151 	/**
152 	 * Compare two strings, honoring case.
153 	 * <p>
154 	 * This method does not honor the JVM locale, but instead always behaves as
155 	 * though it is in the US-ASCII locale.
156 	 *
157 	 * @param a
158 	 *            first string to compare.
159 	 * @param b
160 	 *            second string to compare.
161 	 * @since 2.0
162 	 * @return an int.
163 	 */
164 	public static int compareWithCase(String a, String b) {
165 		for (int i = 0; i < a.length() && i < b.length(); i++) {
166 			int d = a.charAt(i) - b.charAt(i);
167 			if (d != 0)
168 				return d;
169 		}
170 		return a.length() - b.length();
171 	}
172 
173 	/**
174 	 * Parse a string as a standard Git boolean value. See
175 	 * {@link #toBooleanOrNull(String)}.
176 	 *
177 	 * @param stringValue
178 	 *            the string to parse.
179 	 * @return the boolean interpretation of {@code value}.
180 	 * @throws java.lang.IllegalArgumentException
181 	 *             if {@code value} is not recognized as one of the standard
182 	 *             boolean names.
183 	 */
184 	public static boolean toBoolean(String stringValue) {
185 		if (stringValue == null)
186 			throw new NullPointerException(JGitText.get().expectedBooleanStringValue);
187 
188 		final Boolean bool = toBooleanOrNull(stringValue);
189 		if (bool == null)
190 			throw new IllegalArgumentException(MessageFormat.format(JGitText.get().notABoolean, stringValue));
191 
192 		return bool.booleanValue();
193 	}
194 
195 	/**
196 	 * Parse a string as a standard Git boolean value.
197 	 * <p>
198 	 * The terms {@code yes}, {@code true}, {@code 1}, {@code on} can all be
199 	 * used to mean {@code true}.
200 	 * <p>
201 	 * The terms {@code no}, {@code false}, {@code 0}, {@code off} can all be
202 	 * used to mean {@code false}.
203 	 * <p>
204 	 * Comparisons ignore case, via {@link #equalsIgnoreCase(String, String)}.
205 	 *
206 	 * @param stringValue
207 	 *            the string to parse.
208 	 * @return the boolean interpretation of {@code value} or null in case the
209 	 *         string does not represent a boolean value
210 	 */
211 	public static Boolean toBooleanOrNull(String stringValue) {
212 		if (stringValue == null)
213 			return null;
214 
215 		if (equalsIgnoreCase("yes", stringValue) //$NON-NLS-1$
216 				|| equalsIgnoreCase("true", stringValue) //$NON-NLS-1$
217 				|| equalsIgnoreCase("1", stringValue) //$NON-NLS-1$
218 				|| equalsIgnoreCase("on", stringValue)) //$NON-NLS-1$
219 			return Boolean.TRUE;
220 		else if (equalsIgnoreCase("no", stringValue) //$NON-NLS-1$
221 				|| equalsIgnoreCase("false", stringValue) //$NON-NLS-1$
222 				|| equalsIgnoreCase("0", stringValue) //$NON-NLS-1$
223 				|| equalsIgnoreCase("off", stringValue)) //$NON-NLS-1$
224 			return Boolean.FALSE;
225 		else
226 			return null;
227 	}
228 
229 	/**
230 	 * Join a collection of Strings together using the specified separator.
231 	 *
232 	 * @param parts
233 	 *            Strings to join
234 	 * @param separator
235 	 *            used to join
236 	 * @return a String with all the joined parts
237 	 */
238 	public static String join(Collection<String> parts, String separator) {
239 		return StringUtils.join(parts, separator, separator);
240 	}
241 
242 	/**
243 	 * Join a collection of Strings together using the specified separator and a
244 	 * lastSeparator which is used for joining the second last and the last
245 	 * part.
246 	 *
247 	 * @param parts
248 	 *            Strings to join
249 	 * @param separator
250 	 *            separator used to join all but the two last elements
251 	 * @param lastSeparator
252 	 *            separator to use for joining the last two elements
253 	 * @return a String with all the joined parts
254 	 */
255 	public static String join(Collection<String> parts, String separator,
256 			String lastSeparator) {
257 		StringBuilder sb = new StringBuilder();
258 		int i = 0;
259 		int lastIndex = parts.size() - 1;
260 		for (String part : parts) {
261 			sb.append(part);
262 			if (i == lastIndex - 1) {
263 				sb.append(lastSeparator);
264 			} else if (i != lastIndex) {
265 				sb.append(separator);
266 			}
267 			i++;
268 		}
269 		return sb.toString();
270 	}
271 
272 	private StringUtils() {
273 		// Do not create instances
274 	}
275 
276 	/**
277 	 * Test if a string is empty or null.
278 	 *
279 	 * @param stringValue
280 	 *            the string to check
281 	 * @return <code>true</code> if the string is <code>null</code> or empty
282 	 */
283 	public static boolean isEmptyOrNull(String stringValue) {
284 		return stringValue == null || stringValue.length() == 0;
285 	}
286 
287 	/**
288 	 * Replace CRLF, CR or LF with a single space.
289 	 *
290 	 * @param in
291 	 *            A string with line breaks
292 	 * @return in without line breaks
293 	 * @since 3.1
294 	 */
295 	public static String replaceLineBreaksWithSpace(String in) {
296 		char[] buf = new char[in.length()];
297 		int o = 0;
298 		for (int i = 0; i < buf.length; ++i) {
299 			char ch = in.charAt(i);
300 			switch (ch) {
301 			case '\r':
302 				if (i + 1 < buf.length && in.charAt(i + 1) == '\n') {
303 					buf[o++] = ' ';
304 					++i;
305 				} else
306 					buf[o++] = ' ';
307 				break;
308 			case '\n':
309 				buf[o++] = ' ';
310 				break;
311 			default:
312 				buf[o++] = ch;
313 				break;
314 			}
315 		}
316 		return new String(buf, 0, o);
317 	}
318 
319 	/**
320 	 * Parses a number with optional case-insensitive suffix 'k', 'm', or 'g'
321 	 * indicating KiB, MiB, and GiB, respectively. The suffix may follow the
322 	 * number with optional separation by one or more blanks.
323 	 *
324 	 * @param value
325 	 *            {@link String} to parse; with leading and trailing whitespace
326 	 *            ignored
327 	 * @param positiveOnly
328 	 *            {@code true} to only accept positive numbers, {@code false} to
329 	 *            allow negative numbers, too
330 	 * @return the value parsed
331 	 * @throws NumberFormatException
332 	 *             if the {@value} is not parseable, or beyond the range of
333 	 *             {@link Long}
334 	 * @throws StringIndexOutOfBoundsException
335 	 *             if the string is empty or contains only whitespace, or
336 	 *             contains only the letter 'k', 'm', or 'g'
337 	 * @since 6.0
338 	 */
339 	public static long parseLongWithSuffix(@NonNull String value,
340 			boolean positiveOnly)
341 			throws NumberFormatException, StringIndexOutOfBoundsException {
342 		String n = value.strip();
343 		if (n.isEmpty()) {
344 			throw new StringIndexOutOfBoundsException();
345 		}
346 		long mul = 1;
347 		switch (n.charAt(n.length() - 1)) {
348 		case 'g':
349 		case 'G':
350 			mul = GiB;
351 			break;
352 		case 'm':
353 		case 'M':
354 			mul = MiB;
355 			break;
356 		case 'k':
357 		case 'K':
358 			mul = KiB;
359 			break;
360 		default:
361 			break;
362 		}
363 		if (mul > 1) {
364 			n = n.substring(0, n.length() - 1).trim();
365 		}
366 		if (n.isEmpty()) {
367 			throw new StringIndexOutOfBoundsException();
368 		}
369 		long number;
370 		if (positiveOnly) {
371 			number = Long.parseUnsignedLong(n);
372 			if (number < 0) {
373 				throw new NumberFormatException(
374 						MessageFormat.format(JGitText.get().valueExceedsRange,
375 								value, Long.class.getSimpleName()));
376 			}
377 		} else {
378 			number = Long.parseLong(n);
379 		}
380 		if (mul == 1) {
381 			return number;
382 		}
383 		try {
384 			return Math.multiplyExact(mul, number);
385 		} catch (ArithmeticException e) {
386 			NumberFormatException nfe = new NumberFormatException(
387 					e.getLocalizedMessage());
388 			nfe.initCause(e);
389 			throw nfe;
390 		}
391 	}
392 
393 	/**
394 	 * Parses a number with optional case-insensitive suffix 'k', 'm', or 'g'
395 	 * indicating KiB, MiB, and GiB, respectively. The suffix may follow the
396 	 * number with optional separation by blanks.
397 	 *
398 	 * @param value
399 	 *            {@link String} to parse; with leading and trailing whitespace
400 	 *            ignored
401 	 * @param positiveOnly
402 	 *            {@code true} to only accept positive numbers, {@code false} to
403 	 *            allow negative numbers, too
404 	 * @return the value parsed
405 	 * @throws NumberFormatException
406 	 *             if the {@value} is not parseable or beyond the range of
407 	 *             {@link Integer}
408 	 * @throws StringIndexOutOfBoundsException
409 	 *             if the string is empty or contains only whitespace, or
410 	 *             contains only the letter 'k', 'm', or 'g'
411 	 * @since 6.0
412 	 */
413 	public static int parseIntWithSuffix(@NonNull String value,
414 			boolean positiveOnly)
415 			throws NumberFormatException, StringIndexOutOfBoundsException {
416 		try {
417 			return Math.toIntExact(parseLongWithSuffix(value, positiveOnly));
418 		} catch (ArithmeticException e) {
419 			NumberFormatException nfe = new NumberFormatException(
420 					MessageFormat.format(JGitText.get().valueExceedsRange,
421 							value, Integer.class.getSimpleName()));
422 			nfe.initCause(e);
423 			throw nfe;
424 		}
425 	}
426 
427 	/**
428 	 * Formats an integral value as a decimal number with 'k', 'm', or 'g'
429 	 * suffix if it is an exact multiple of 1024, otherwise returns the value
430 	 * representation as a decimal number without suffix.
431 	 *
432 	 * @param value
433 	 *            Value to format
434 	 * @return the value's String representation
435 	 * @since 6.0
436 	 */
437 	public static String formatWithSuffix(long value) {
438 		if (value >= GiB && (value % GiB) == 0) {
439 			return String.valueOf(value / GiB) + 'g';
440 		}
441 		if (value >= MiB && (value % MiB) == 0) {
442 			return String.valueOf(value / MiB) + 'm';
443 		}
444 		if (value >= KiB && (value % KiB) == 0) {
445 			return String.valueOf(value / KiB) + 'k';
446 		}
447 		return String.valueOf(value);
448 	}
449 }