|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.sync; |
|
6 |
|
7 import java.io.BufferedReader; |
|
8 import java.io.FileInputStream; |
|
9 import java.io.IOException; |
|
10 import java.io.InputStreamReader; |
|
11 import java.io.UnsupportedEncodingException; |
|
12 import java.math.BigDecimal; |
|
13 import java.math.BigInteger; |
|
14 import java.net.URLDecoder; |
|
15 import java.security.MessageDigest; |
|
16 import java.security.NoSuchAlgorithmException; |
|
17 import java.security.SecureRandom; |
|
18 import java.text.DecimalFormat; |
|
19 import java.util.ArrayList; |
|
20 import java.util.Collection; |
|
21 import java.util.HashMap; |
|
22 import java.util.HashSet; |
|
23 import java.util.Locale; |
|
24 import java.util.Map; |
|
25 import java.util.TreeMap; |
|
26 |
|
27 import org.json.simple.JSONArray; |
|
28 import org.mozilla.apache.commons.codec.binary.Base32; |
|
29 import org.mozilla.apache.commons.codec.binary.Base64; |
|
30 import org.mozilla.gecko.background.common.log.Logger; |
|
31 import org.mozilla.gecko.background.nativecode.NativeCrypto; |
|
32 import org.mozilla.gecko.sync.setup.Constants; |
|
33 |
|
34 import android.annotation.SuppressLint; |
|
35 import android.content.Context; |
|
36 import android.content.SharedPreferences; |
|
37 import android.os.Bundle; |
|
38 |
|
39 public class Utils { |
|
40 |
|
41 private static final String LOG_TAG = "Utils"; |
|
42 |
|
43 private static SecureRandom sharedSecureRandom = new SecureRandom(); |
|
44 |
|
45 // See <http://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29> |
|
46 public static final int SHARED_PREFERENCES_MODE = 0; |
|
47 |
|
48 public static String generateGuid() { |
|
49 byte[] encodedBytes = Base64.encodeBase64(generateRandomBytes(9), false); |
|
50 return new String(encodedBytes).replace("+", "-").replace("/", "_"); |
|
51 } |
|
52 |
|
53 /** |
|
54 * Helper to generate secure random bytes. |
|
55 * |
|
56 * @param length |
|
57 * Number of bytes to generate. |
|
58 */ |
|
59 public static byte[] generateRandomBytes(int length) { |
|
60 byte[] bytes = new byte[length]; |
|
61 sharedSecureRandom.nextBytes(bytes); |
|
62 return bytes; |
|
63 } |
|
64 |
|
65 /** |
|
66 * Helper to generate a random integer in a specified range. |
|
67 * |
|
68 * @param r |
|
69 * Generate an integer between 0 and r-1 inclusive. |
|
70 */ |
|
71 public static BigInteger generateBigIntegerLessThan(BigInteger r) { |
|
72 int maxBytes = (int) Math.ceil(((double) r.bitLength()) / 8); |
|
73 BigInteger randInt = new BigInteger(generateRandomBytes(maxBytes)); |
|
74 return randInt.mod(r); |
|
75 } |
|
76 |
|
77 /** |
|
78 * Helper to reseed the shared secure random number generator. |
|
79 */ |
|
80 public static void reseedSharedRandom() { |
|
81 sharedSecureRandom.setSeed(sharedSecureRandom.generateSeed(8)); |
|
82 } |
|
83 |
|
84 /** |
|
85 * Helper to convert a byte array to a hex-encoded string |
|
86 */ |
|
87 public static String byte2Hex(final byte[] b) { |
|
88 return byte2Hex(b, 2 * b.length); |
|
89 } |
|
90 |
|
91 public static String byte2Hex(final byte[] b, int hexLength) { |
|
92 final StringBuilder hs = new StringBuilder(Math.max(2*b.length, hexLength)); |
|
93 String stmp; |
|
94 |
|
95 for (int n = 0; n < hexLength - 2*b.length; n++) { |
|
96 hs.append("0"); |
|
97 } |
|
98 |
|
99 for (int n = 0; n < b.length; n++) { |
|
100 stmp = Integer.toHexString(b[n] & 0XFF); |
|
101 |
|
102 if (stmp.length() == 1) { |
|
103 hs.append("0"); |
|
104 } |
|
105 hs.append(stmp); |
|
106 } |
|
107 |
|
108 return hs.toString(); |
|
109 } |
|
110 |
|
111 public static byte[] concatAll(byte[] first, byte[]... rest) { |
|
112 int totalLength = first.length; |
|
113 for (byte[] array : rest) { |
|
114 totalLength += array.length; |
|
115 } |
|
116 |
|
117 byte[] result = new byte[totalLength]; |
|
118 int offset = first.length; |
|
119 |
|
120 System.arraycopy(first, 0, result, 0, offset); |
|
121 |
|
122 for (byte[] array : rest) { |
|
123 System.arraycopy(array, 0, result, offset, array.length); |
|
124 offset += array.length; |
|
125 } |
|
126 return result; |
|
127 } |
|
128 |
|
129 /** |
|
130 * Utility for Base64 decoding. Should ensure that the correct |
|
131 * Apache Commons version is used. |
|
132 * |
|
133 * @param base64 |
|
134 * An input string. Will be decoded as UTF-8. |
|
135 * @return |
|
136 * A byte array of decoded values. |
|
137 * @throws UnsupportedEncodingException |
|
138 * Should not occur. |
|
139 */ |
|
140 public static byte[] decodeBase64(String base64) throws UnsupportedEncodingException { |
|
141 return Base64.decodeBase64(base64.getBytes("UTF-8")); |
|
142 } |
|
143 |
|
144 @SuppressLint("DefaultLocale") |
|
145 public static byte[] decodeFriendlyBase32(String base32) { |
|
146 Base32 converter = new Base32(); |
|
147 final String translated = base32.replace('8', 'l').replace('9', 'o'); |
|
148 return converter.decode(translated.toUpperCase()); |
|
149 } |
|
150 |
|
151 public static byte[] hex2Byte(String str, int byteLength) { |
|
152 byte[] second = hex2Byte(str); |
|
153 if (second.length >= byteLength) { |
|
154 return second; |
|
155 } |
|
156 // New Java arrays are zeroed: |
|
157 // http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5 |
|
158 byte[] first = new byte[byteLength - second.length]; |
|
159 return Utils.concatAll(first, second); |
|
160 } |
|
161 |
|
162 public static byte[] hex2Byte(String str) { |
|
163 if (str.length() % 2 == 1) { |
|
164 str = "0" + str; |
|
165 } |
|
166 |
|
167 byte[] bytes = new byte[str.length() / 2]; |
|
168 for (int i = 0; i < bytes.length; i++) { |
|
169 bytes[i] = (byte) Integer.parseInt(str.substring(2 * i, 2 * i + 2), 16); |
|
170 } |
|
171 return bytes; |
|
172 } |
|
173 |
|
174 public static String millisecondsToDecimalSecondsString(long ms) { |
|
175 return millisecondsToDecimalSeconds(ms).toString(); |
|
176 } |
|
177 |
|
178 // For dumping into JSON without quotes. |
|
179 public static BigDecimal millisecondsToDecimalSeconds(long ms) { |
|
180 return new BigDecimal(ms).movePointLeft(3); |
|
181 } |
|
182 |
|
183 // This lives until Bug 708956 lands, and we don't have to do it any more. |
|
184 public static long decimalSecondsToMilliseconds(String decimal) { |
|
185 try { |
|
186 return new BigDecimal(decimal).movePointRight(3).longValue(); |
|
187 } catch (Exception e) { |
|
188 return -1; |
|
189 } |
|
190 } |
|
191 |
|
192 // Oh, Java. |
|
193 public static long decimalSecondsToMilliseconds(Double decimal) { |
|
194 // Truncates towards 0. |
|
195 return (long)(decimal * 1000); |
|
196 } |
|
197 |
|
198 public static long decimalSecondsToMilliseconds(Long decimal) { |
|
199 return decimal * 1000; |
|
200 } |
|
201 |
|
202 public static long decimalSecondsToMilliseconds(Integer decimal) { |
|
203 return (long)(decimal * 1000); |
|
204 } |
|
205 |
|
206 public static byte[] sha256(byte[] in) |
|
207 throws NoSuchAlgorithmException { |
|
208 MessageDigest sha1 = MessageDigest.getInstance("SHA-256"); |
|
209 return sha1.digest(in); |
|
210 } |
|
211 |
|
212 protected static byte[] sha1(final String utf8) |
|
213 throws NoSuchAlgorithmException, UnsupportedEncodingException { |
|
214 final byte[] bytes = utf8.getBytes("UTF-8"); |
|
215 try { |
|
216 return NativeCrypto.sha1(bytes); |
|
217 } catch (final LinkageError e) { |
|
218 // This will throw UnsatisifiedLinkError (missing mozglue) the first time it is called, and |
|
219 // ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this |
|
220 // is called; LinkageError is their common ancestor. |
|
221 Logger.warn(LOG_TAG, "Got throwable stretching password using native sha1 implementation; " + |
|
222 "ignoring and using Java implementation.", e); |
|
223 final MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); |
|
224 return sha1.digest(utf8.getBytes("UTF-8")); |
|
225 } |
|
226 } |
|
227 |
|
228 protected static String sha1Base32(final String utf8) |
|
229 throws NoSuchAlgorithmException, UnsupportedEncodingException { |
|
230 return new Base32().encodeAsString(sha1(utf8)).toLowerCase(Locale.US); |
|
231 } |
|
232 |
|
233 /** |
|
234 * If we encounter characters not allowed by the API (as found for |
|
235 * instance in an email address), hash the value. |
|
236 * @param account |
|
237 * An account string. |
|
238 * @return |
|
239 * An acceptable string. |
|
240 * @throws UnsupportedEncodingException |
|
241 * @throws NoSuchAlgorithmException |
|
242 */ |
|
243 public static String usernameFromAccount(final String account) throws NoSuchAlgorithmException, UnsupportedEncodingException { |
|
244 if (account == null || account.equals("")) { |
|
245 throw new IllegalArgumentException("No account name provided."); |
|
246 } |
|
247 if (account.matches("^[A-Za-z0-9._-]+$")) { |
|
248 return account.toLowerCase(Locale.US); |
|
249 } |
|
250 return sha1Base32(account.toLowerCase(Locale.US)); |
|
251 } |
|
252 |
|
253 public static SharedPreferences getSharedPreferences(final Context context, final String product, final String username, final String serverURL, final String profile, final long version) |
|
254 throws NoSuchAlgorithmException, UnsupportedEncodingException { |
|
255 String prefsPath = getPrefsPath(product, username, serverURL, profile, version); |
|
256 return context.getSharedPreferences(prefsPath, SHARED_PREFERENCES_MODE); |
|
257 } |
|
258 |
|
259 /** |
|
260 * Get shared preferences path for a Sync account. |
|
261 * |
|
262 * @param product the Firefox Sync product package name (like "org.mozilla.firefox"). |
|
263 * @param username the Sync account name, optionally encoded with <code>Utils.usernameFromAccount</code>. |
|
264 * @param serverURL the Sync account server URL. |
|
265 * @param profile the Firefox profile name. |
|
266 * @param version the version of preferences to reference. |
|
267 * @return the path. |
|
268 * @throws NoSuchAlgorithmException |
|
269 * @throws UnsupportedEncodingException |
|
270 */ |
|
271 public static String getPrefsPath(final String product, final String username, final String serverURL, final String profile, final long version) |
|
272 throws NoSuchAlgorithmException, UnsupportedEncodingException { |
|
273 final String encodedAccount = sha1Base32(serverURL + ":" + usernameFromAccount(username)); |
|
274 |
|
275 if (version <= 0) { |
|
276 return "sync.prefs." + encodedAccount; |
|
277 } else { |
|
278 final String sanitizedProduct = product.replace('.', '!').replace(' ', '!'); |
|
279 return "sync.prefs." + sanitizedProduct + "." + encodedAccount + "." + profile + "." + version; |
|
280 } |
|
281 } |
|
282 |
|
283 public static void addToIndexBucketMap(TreeMap<Long, ArrayList<String>> map, long index, String value) { |
|
284 ArrayList<String> bucket = map.get(index); |
|
285 if (bucket == null) { |
|
286 bucket = new ArrayList<String>(); |
|
287 } |
|
288 bucket.add(value); |
|
289 map.put(index, bucket); |
|
290 } |
|
291 |
|
292 /** |
|
293 * Yes, an equality method that's null-safe. |
|
294 */ |
|
295 private static boolean same(Object a, Object b) { |
|
296 if (a == b) { |
|
297 return true; |
|
298 } |
|
299 if (a == null || b == null) { |
|
300 return false; // If both null, case above applies. |
|
301 } |
|
302 return a.equals(b); |
|
303 } |
|
304 |
|
305 /** |
|
306 * Return true if the two arrays are both null, or are both arrays |
|
307 * containing the same elements in the same order. |
|
308 */ |
|
309 public static boolean sameArrays(JSONArray a, JSONArray b) { |
|
310 if (a == b) { |
|
311 return true; |
|
312 } |
|
313 if (a == null || b == null) { |
|
314 return false; |
|
315 } |
|
316 final int size = a.size(); |
|
317 if (size != b.size()) { |
|
318 return false; |
|
319 } |
|
320 for (int i = 0; i < size; ++i) { |
|
321 if (!same(a.get(i), b.get(i))) { |
|
322 return false; |
|
323 } |
|
324 } |
|
325 return true; |
|
326 } |
|
327 |
|
328 /** |
|
329 * Takes a URI, extracting URI components. |
|
330 * @param scheme the URI scheme on which to match. |
|
331 */ |
|
332 @SuppressWarnings("deprecation") |
|
333 public static Map<String, String> extractURIComponents(String scheme, String uri) { |
|
334 if (uri.indexOf(scheme) != 0) { |
|
335 throw new IllegalArgumentException("URI scheme does not match: " + scheme); |
|
336 } |
|
337 |
|
338 // Do this the hard way to avoid taking a large dependency on |
|
339 // HttpClient or getting all regex-tastic. |
|
340 String components = uri.substring(scheme.length()); |
|
341 HashMap<String, String> out = new HashMap<String, String>(); |
|
342 String[] parts = components.split("&"); |
|
343 for (int i = 0; i < parts.length; ++i) { |
|
344 String part = parts[i]; |
|
345 if (part.length() == 0) { |
|
346 continue; |
|
347 } |
|
348 String[] pair = part.split("=", 2); |
|
349 switch (pair.length) { |
|
350 case 0: |
|
351 continue; |
|
352 case 1: |
|
353 out.put(URLDecoder.decode(pair[0]), null); |
|
354 break; |
|
355 case 2: |
|
356 out.put(URLDecoder.decode(pair[0]), URLDecoder.decode(pair[1])); |
|
357 break; |
|
358 } |
|
359 } |
|
360 return out; |
|
361 } |
|
362 |
|
363 // Because TextUtils.join is not stubbed. |
|
364 public static String toDelimitedString(String delimiter, Collection<? extends Object> items) { |
|
365 if (items == null || items.size() == 0) { |
|
366 return ""; |
|
367 } |
|
368 |
|
369 StringBuilder sb = new StringBuilder(); |
|
370 int i = 0; |
|
371 int c = items.size(); |
|
372 for (Object object : items) { |
|
373 sb.append(object.toString()); |
|
374 if (++i < c) { |
|
375 sb.append(delimiter); |
|
376 } |
|
377 } |
|
378 return sb.toString(); |
|
379 } |
|
380 |
|
381 public static String toCommaSeparatedString(Collection<? extends Object> items) { |
|
382 return toDelimitedString(", ", items); |
|
383 } |
|
384 |
|
385 /** |
|
386 * Names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP). |
|
387 * |
|
388 * @param knownStageNames collection of known stage names (set ALL above). |
|
389 * @param toSync set SYNC above, or <code>null</code> to sync all known stages. |
|
390 * @param toSkip set SKIP above, or <code>null</code> to not skip any stages. |
|
391 * @return stage names. |
|
392 */ |
|
393 public static Collection<String> getStagesToSync(final Collection<String> knownStageNames, Collection<String> toSync, Collection<String> toSkip) { |
|
394 if (toSkip == null) { |
|
395 toSkip = new HashSet<String>(); |
|
396 } else { |
|
397 toSkip = new HashSet<String>(toSkip); |
|
398 } |
|
399 |
|
400 if (toSync == null) { |
|
401 toSync = new HashSet<String>(knownStageNames); |
|
402 } else { |
|
403 toSync = new HashSet<String>(toSync); |
|
404 } |
|
405 toSync.retainAll(knownStageNames); |
|
406 toSync.removeAll(toSkip); |
|
407 return toSync; |
|
408 } |
|
409 |
|
410 /** |
|
411 * Get names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP). |
|
412 * |
|
413 * @param knownStageNames collection of known stage names (set ALL above). |
|
414 * @param extras |
|
415 * a <code>Bundle</code> instance (possibly null) optionally containing keys |
|
416 * <code>EXTRAS_KEY_STAGES_TO_SYNC</code> (set SYNC above) and |
|
417 * <code>EXTRAS_KEY_STAGES_TO_SKIP</code> (set SKIP above). |
|
418 * @return stage names. |
|
419 */ |
|
420 public static Collection<String> getStagesToSyncFromBundle(final Collection<String> knownStageNames, final Bundle extras) { |
|
421 if (extras == null) { |
|
422 return knownStageNames; |
|
423 } |
|
424 String toSyncString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SYNC); |
|
425 String toSkipString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SKIP); |
|
426 if (toSyncString == null && toSkipString == null) { |
|
427 return knownStageNames; |
|
428 } |
|
429 |
|
430 ArrayList<String> toSync = null; |
|
431 ArrayList<String> toSkip = null; |
|
432 if (toSyncString != null) { |
|
433 try { |
|
434 toSync = new ArrayList<String>(ExtendedJSONObject.parseJSONObject(toSyncString).keySet()); |
|
435 } catch (Exception e) { |
|
436 Logger.warn(LOG_TAG, "Got exception parsing stages to sync: '" + toSyncString + "'.", e); |
|
437 } |
|
438 } |
|
439 if (toSkipString != null) { |
|
440 try { |
|
441 toSkip = new ArrayList<String>(ExtendedJSONObject.parseJSONObject(toSkipString).keySet()); |
|
442 } catch (Exception e) { |
|
443 Logger.warn(LOG_TAG, "Got exception parsing stages to skip: '" + toSkipString + "'.", e); |
|
444 } |
|
445 } |
|
446 |
|
447 Logger.info(LOG_TAG, "Asked to sync '" + Utils.toCommaSeparatedString(toSync) + |
|
448 "' and to skip '" + Utils.toCommaSeparatedString(toSkip) + "'."); |
|
449 return getStagesToSync(knownStageNames, toSync, toSkip); |
|
450 } |
|
451 |
|
452 /** |
|
453 * Put names of stages to sync and to skip into sync extras bundle. |
|
454 * |
|
455 * @param bundle |
|
456 * a <code>Bundle</code> instance (possibly null). |
|
457 * @param stagesToSync |
|
458 * collection of stage names to sync: key |
|
459 * <code>EXTRAS_KEY_STAGES_TO_SYNC</code>; ignored if <code>null</code>. |
|
460 * @param stagesToSkip |
|
461 * collection of stage names to skip: key |
|
462 * <code>EXTRAS_KEY_STAGES_TO_SKIP</code>; ignored if <code>null</code>. |
|
463 */ |
|
464 public static void putStageNamesToSync(final Bundle bundle, final String[] stagesToSync, final String[] stagesToSkip) { |
|
465 if (bundle == null) { |
|
466 return; |
|
467 } |
|
468 |
|
469 if (stagesToSync != null) { |
|
470 ExtendedJSONObject o = new ExtendedJSONObject(); |
|
471 for (String stageName : stagesToSync) { |
|
472 o.put(stageName, 0); |
|
473 } |
|
474 bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SYNC, o.toJSONString()); |
|
475 } |
|
476 |
|
477 if (stagesToSkip != null) { |
|
478 ExtendedJSONObject o = new ExtendedJSONObject(); |
|
479 for (String stageName : stagesToSkip) { |
|
480 o.put(stageName, 0); |
|
481 } |
|
482 bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SKIP, o.toJSONString()); |
|
483 } |
|
484 } |
|
485 |
|
486 /** |
|
487 * Read contents of file as a string. |
|
488 * |
|
489 * @param context Android context. |
|
490 * @param filename name of file to read; must not be null. |
|
491 * @return <code>String</code> instance. |
|
492 */ |
|
493 public static String readFile(final Context context, final String filename) { |
|
494 if (filename == null) { |
|
495 throw new IllegalArgumentException("Passed null filename in readFile."); |
|
496 } |
|
497 |
|
498 FileInputStream fis = null; |
|
499 InputStreamReader isr = null; |
|
500 BufferedReader br = null; |
|
501 |
|
502 try { |
|
503 fis = context.openFileInput(filename); |
|
504 isr = new InputStreamReader(fis); |
|
505 br = new BufferedReader(isr); |
|
506 StringBuilder sb = new StringBuilder(); |
|
507 String line; |
|
508 while ((line = br.readLine()) != null) { |
|
509 sb.append(line); |
|
510 } |
|
511 return sb.toString(); |
|
512 } catch (Exception e) { |
|
513 return null; |
|
514 } finally { |
|
515 if (isr != null) { |
|
516 try { |
|
517 isr.close(); |
|
518 } catch (IOException e) { |
|
519 // Ignore. |
|
520 } |
|
521 } |
|
522 if (fis != null) { |
|
523 try { |
|
524 fis.close(); |
|
525 } catch (IOException e) { |
|
526 // Ignore. |
|
527 } |
|
528 } |
|
529 } |
|
530 } |
|
531 |
|
532 /** |
|
533 * Format a duration as a string, like "0.56 seconds". |
|
534 * |
|
535 * @param startMillis start time in milliseconds. |
|
536 * @param endMillis end time in milliseconds. |
|
537 * @return formatted string. |
|
538 */ |
|
539 public static String formatDuration(long startMillis, long endMillis) { |
|
540 final long duration = endMillis - startMillis; |
|
541 return new DecimalFormat("#0.00 seconds").format(((double) duration) / 1000); |
|
542 } |
|
543 |
|
544 /** |
|
545 * This will take a string containing a UTF-8 representation of a UTF-8 |
|
546 * byte array — e.g., "pïgéons1" — and return UTF-8 (e.g., "pïgéons1"). |
|
547 * |
|
548 * This is the format produced by desktop Firefox when exchanging credentials |
|
549 * containing non-ASCII characters. |
|
550 */ |
|
551 public static String decodeUTF8(final String in) throws UnsupportedEncodingException { |
|
552 final int length = in.length(); |
|
553 final byte[] asciiBytes = new byte[length]; |
|
554 for (int i = 0; i < length; ++i) { |
|
555 asciiBytes[i] = (byte) in.codePointAt(i); |
|
556 } |
|
557 return new String(asciiBytes, "UTF-8"); |
|
558 } |
|
559 |
|
560 /** |
|
561 * Replace "foo@bar.com" with "XXX@XXX.XXX". |
|
562 */ |
|
563 public static String obfuscateEmail(final String in) { |
|
564 return in.replaceAll("[^@\\.]", "X"); |
|
565 } |
|
566 |
|
567 public static String nodeWeaveURL(String serverURL, String username) { |
|
568 String userPart = username + "/node/weave"; |
|
569 if (serverURL == null) { |
|
570 return SyncConstants.DEFAULT_AUTH_SERVER + "user/1.0/" + userPart; |
|
571 } |
|
572 if (!serverURL.endsWith("/")) { |
|
573 serverURL = serverURL + "/"; |
|
574 } |
|
575 return serverURL + "user/1.0/" + userPart; |
|
576 } |
|
577 |
|
578 public static void throwIfNull(Object... objects) { |
|
579 for (Object object : objects) { |
|
580 if (object == null) { |
|
581 throw new IllegalArgumentException("object must not be null"); |
|
582 } |
|
583 } |
|
584 } |
|
585 |
|
586 /** |
|
587 * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale} |
|
588 * stringifies as "es_ES". |
|
589 * |
|
590 * This method approximates the Java 7 method <code>Locale#toLanguageTag()</code>. |
|
591 * <p> |
|
592 * <b>Warning:</b> all consumers of this method will need to be audited when |
|
593 * we have active locale switching. |
|
594 * |
|
595 * @return a locale string suitable for passing to Gecko. |
|
596 */ |
|
597 public static String getLanguageTag(final Locale locale) { |
|
598 // If this were Java 7: |
|
599 // return locale.toLanguageTag(); |
|
600 |
|
601 String language = locale.getLanguage(); // Can, but should never be, an empty string. |
|
602 // Modernize certain language codes. |
|
603 if (language.equals("iw")) { |
|
604 language = "he"; |
|
605 } else if (language.equals("in")) { |
|
606 language = "id"; |
|
607 } else if (language.equals("ji")) { |
|
608 language = "yi"; |
|
609 } |
|
610 |
|
611 String country = locale.getCountry(); // Can be an empty string. |
|
612 if (country.equals("")) { |
|
613 return language; |
|
614 } |
|
615 return language + "-" + country; |
|
616 } |
|
617 } |