1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/sync/Utils.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,617 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.sync; 1.9 + 1.10 +import java.io.BufferedReader; 1.11 +import java.io.FileInputStream; 1.12 +import java.io.IOException; 1.13 +import java.io.InputStreamReader; 1.14 +import java.io.UnsupportedEncodingException; 1.15 +import java.math.BigDecimal; 1.16 +import java.math.BigInteger; 1.17 +import java.net.URLDecoder; 1.18 +import java.security.MessageDigest; 1.19 +import java.security.NoSuchAlgorithmException; 1.20 +import java.security.SecureRandom; 1.21 +import java.text.DecimalFormat; 1.22 +import java.util.ArrayList; 1.23 +import java.util.Collection; 1.24 +import java.util.HashMap; 1.25 +import java.util.HashSet; 1.26 +import java.util.Locale; 1.27 +import java.util.Map; 1.28 +import java.util.TreeMap; 1.29 + 1.30 +import org.json.simple.JSONArray; 1.31 +import org.mozilla.apache.commons.codec.binary.Base32; 1.32 +import org.mozilla.apache.commons.codec.binary.Base64; 1.33 +import org.mozilla.gecko.background.common.log.Logger; 1.34 +import org.mozilla.gecko.background.nativecode.NativeCrypto; 1.35 +import org.mozilla.gecko.sync.setup.Constants; 1.36 + 1.37 +import android.annotation.SuppressLint; 1.38 +import android.content.Context; 1.39 +import android.content.SharedPreferences; 1.40 +import android.os.Bundle; 1.41 + 1.42 +public class Utils { 1.43 + 1.44 + private static final String LOG_TAG = "Utils"; 1.45 + 1.46 + private static SecureRandom sharedSecureRandom = new SecureRandom(); 1.47 + 1.48 + // See <http://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29> 1.49 + public static final int SHARED_PREFERENCES_MODE = 0; 1.50 + 1.51 + public static String generateGuid() { 1.52 + byte[] encodedBytes = Base64.encodeBase64(generateRandomBytes(9), false); 1.53 + return new String(encodedBytes).replace("+", "-").replace("/", "_"); 1.54 + } 1.55 + 1.56 + /** 1.57 + * Helper to generate secure random bytes. 1.58 + * 1.59 + * @param length 1.60 + * Number of bytes to generate. 1.61 + */ 1.62 + public static byte[] generateRandomBytes(int length) { 1.63 + byte[] bytes = new byte[length]; 1.64 + sharedSecureRandom.nextBytes(bytes); 1.65 + return bytes; 1.66 + } 1.67 + 1.68 + /** 1.69 + * Helper to generate a random integer in a specified range. 1.70 + * 1.71 + * @param r 1.72 + * Generate an integer between 0 and r-1 inclusive. 1.73 + */ 1.74 + public static BigInteger generateBigIntegerLessThan(BigInteger r) { 1.75 + int maxBytes = (int) Math.ceil(((double) r.bitLength()) / 8); 1.76 + BigInteger randInt = new BigInteger(generateRandomBytes(maxBytes)); 1.77 + return randInt.mod(r); 1.78 + } 1.79 + 1.80 + /** 1.81 + * Helper to reseed the shared secure random number generator. 1.82 + */ 1.83 + public static void reseedSharedRandom() { 1.84 + sharedSecureRandom.setSeed(sharedSecureRandom.generateSeed(8)); 1.85 + } 1.86 + 1.87 + /** 1.88 + * Helper to convert a byte array to a hex-encoded string 1.89 + */ 1.90 + public static String byte2Hex(final byte[] b) { 1.91 + return byte2Hex(b, 2 * b.length); 1.92 + } 1.93 + 1.94 + public static String byte2Hex(final byte[] b, int hexLength) { 1.95 + final StringBuilder hs = new StringBuilder(Math.max(2*b.length, hexLength)); 1.96 + String stmp; 1.97 + 1.98 + for (int n = 0; n < hexLength - 2*b.length; n++) { 1.99 + hs.append("0"); 1.100 + } 1.101 + 1.102 + for (int n = 0; n < b.length; n++) { 1.103 + stmp = Integer.toHexString(b[n] & 0XFF); 1.104 + 1.105 + if (stmp.length() == 1) { 1.106 + hs.append("0"); 1.107 + } 1.108 + hs.append(stmp); 1.109 + } 1.110 + 1.111 + return hs.toString(); 1.112 + } 1.113 + 1.114 + public static byte[] concatAll(byte[] first, byte[]... rest) { 1.115 + int totalLength = first.length; 1.116 + for (byte[] array : rest) { 1.117 + totalLength += array.length; 1.118 + } 1.119 + 1.120 + byte[] result = new byte[totalLength]; 1.121 + int offset = first.length; 1.122 + 1.123 + System.arraycopy(first, 0, result, 0, offset); 1.124 + 1.125 + for (byte[] array : rest) { 1.126 + System.arraycopy(array, 0, result, offset, array.length); 1.127 + offset += array.length; 1.128 + } 1.129 + return result; 1.130 + } 1.131 + 1.132 + /** 1.133 + * Utility for Base64 decoding. Should ensure that the correct 1.134 + * Apache Commons version is used. 1.135 + * 1.136 + * @param base64 1.137 + * An input string. Will be decoded as UTF-8. 1.138 + * @return 1.139 + * A byte array of decoded values. 1.140 + * @throws UnsupportedEncodingException 1.141 + * Should not occur. 1.142 + */ 1.143 + public static byte[] decodeBase64(String base64) throws UnsupportedEncodingException { 1.144 + return Base64.decodeBase64(base64.getBytes("UTF-8")); 1.145 + } 1.146 + 1.147 + @SuppressLint("DefaultLocale") 1.148 + public static byte[] decodeFriendlyBase32(String base32) { 1.149 + Base32 converter = new Base32(); 1.150 + final String translated = base32.replace('8', 'l').replace('9', 'o'); 1.151 + return converter.decode(translated.toUpperCase()); 1.152 + } 1.153 + 1.154 + public static byte[] hex2Byte(String str, int byteLength) { 1.155 + byte[] second = hex2Byte(str); 1.156 + if (second.length >= byteLength) { 1.157 + return second; 1.158 + } 1.159 + // New Java arrays are zeroed: 1.160 + // http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5 1.161 + byte[] first = new byte[byteLength - second.length]; 1.162 + return Utils.concatAll(first, second); 1.163 + } 1.164 + 1.165 + public static byte[] hex2Byte(String str) { 1.166 + if (str.length() % 2 == 1) { 1.167 + str = "0" + str; 1.168 + } 1.169 + 1.170 + byte[] bytes = new byte[str.length() / 2]; 1.171 + for (int i = 0; i < bytes.length; i++) { 1.172 + bytes[i] = (byte) Integer.parseInt(str.substring(2 * i, 2 * i + 2), 16); 1.173 + } 1.174 + return bytes; 1.175 + } 1.176 + 1.177 + public static String millisecondsToDecimalSecondsString(long ms) { 1.178 + return millisecondsToDecimalSeconds(ms).toString(); 1.179 + } 1.180 + 1.181 + // For dumping into JSON without quotes. 1.182 + public static BigDecimal millisecondsToDecimalSeconds(long ms) { 1.183 + return new BigDecimal(ms).movePointLeft(3); 1.184 + } 1.185 + 1.186 + // This lives until Bug 708956 lands, and we don't have to do it any more. 1.187 + public static long decimalSecondsToMilliseconds(String decimal) { 1.188 + try { 1.189 + return new BigDecimal(decimal).movePointRight(3).longValue(); 1.190 + } catch (Exception e) { 1.191 + return -1; 1.192 + } 1.193 + } 1.194 + 1.195 + // Oh, Java. 1.196 + public static long decimalSecondsToMilliseconds(Double decimal) { 1.197 + // Truncates towards 0. 1.198 + return (long)(decimal * 1000); 1.199 + } 1.200 + 1.201 + public static long decimalSecondsToMilliseconds(Long decimal) { 1.202 + return decimal * 1000; 1.203 + } 1.204 + 1.205 + public static long decimalSecondsToMilliseconds(Integer decimal) { 1.206 + return (long)(decimal * 1000); 1.207 + } 1.208 + 1.209 + public static byte[] sha256(byte[] in) 1.210 + throws NoSuchAlgorithmException { 1.211 + MessageDigest sha1 = MessageDigest.getInstance("SHA-256"); 1.212 + return sha1.digest(in); 1.213 + } 1.214 + 1.215 + protected static byte[] sha1(final String utf8) 1.216 + throws NoSuchAlgorithmException, UnsupportedEncodingException { 1.217 + final byte[] bytes = utf8.getBytes("UTF-8"); 1.218 + try { 1.219 + return NativeCrypto.sha1(bytes); 1.220 + } catch (final LinkageError e) { 1.221 + // This will throw UnsatisifiedLinkError (missing mozglue) the first time it is called, and 1.222 + // ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this 1.223 + // is called; LinkageError is their common ancestor. 1.224 + Logger.warn(LOG_TAG, "Got throwable stretching password using native sha1 implementation; " + 1.225 + "ignoring and using Java implementation.", e); 1.226 + final MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); 1.227 + return sha1.digest(utf8.getBytes("UTF-8")); 1.228 + } 1.229 + } 1.230 + 1.231 + protected static String sha1Base32(final String utf8) 1.232 + throws NoSuchAlgorithmException, UnsupportedEncodingException { 1.233 + return new Base32().encodeAsString(sha1(utf8)).toLowerCase(Locale.US); 1.234 + } 1.235 + 1.236 + /** 1.237 + * If we encounter characters not allowed by the API (as found for 1.238 + * instance in an email address), hash the value. 1.239 + * @param account 1.240 + * An account string. 1.241 + * @return 1.242 + * An acceptable string. 1.243 + * @throws UnsupportedEncodingException 1.244 + * @throws NoSuchAlgorithmException 1.245 + */ 1.246 + public static String usernameFromAccount(final String account) throws NoSuchAlgorithmException, UnsupportedEncodingException { 1.247 + if (account == null || account.equals("")) { 1.248 + throw new IllegalArgumentException("No account name provided."); 1.249 + } 1.250 + if (account.matches("^[A-Za-z0-9._-]+$")) { 1.251 + return account.toLowerCase(Locale.US); 1.252 + } 1.253 + return sha1Base32(account.toLowerCase(Locale.US)); 1.254 + } 1.255 + 1.256 + public static SharedPreferences getSharedPreferences(final Context context, final String product, final String username, final String serverURL, final String profile, final long version) 1.257 + throws NoSuchAlgorithmException, UnsupportedEncodingException { 1.258 + String prefsPath = getPrefsPath(product, username, serverURL, profile, version); 1.259 + return context.getSharedPreferences(prefsPath, SHARED_PREFERENCES_MODE); 1.260 + } 1.261 + 1.262 + /** 1.263 + * Get shared preferences path for a Sync account. 1.264 + * 1.265 + * @param product the Firefox Sync product package name (like "org.mozilla.firefox"). 1.266 + * @param username the Sync account name, optionally encoded with <code>Utils.usernameFromAccount</code>. 1.267 + * @param serverURL the Sync account server URL. 1.268 + * @param profile the Firefox profile name. 1.269 + * @param version the version of preferences to reference. 1.270 + * @return the path. 1.271 + * @throws NoSuchAlgorithmException 1.272 + * @throws UnsupportedEncodingException 1.273 + */ 1.274 + public static String getPrefsPath(final String product, final String username, final String serverURL, final String profile, final long version) 1.275 + throws NoSuchAlgorithmException, UnsupportedEncodingException { 1.276 + final String encodedAccount = sha1Base32(serverURL + ":" + usernameFromAccount(username)); 1.277 + 1.278 + if (version <= 0) { 1.279 + return "sync.prefs." + encodedAccount; 1.280 + } else { 1.281 + final String sanitizedProduct = product.replace('.', '!').replace(' ', '!'); 1.282 + return "sync.prefs." + sanitizedProduct + "." + encodedAccount + "." + profile + "." + version; 1.283 + } 1.284 + } 1.285 + 1.286 + public static void addToIndexBucketMap(TreeMap<Long, ArrayList<String>> map, long index, String value) { 1.287 + ArrayList<String> bucket = map.get(index); 1.288 + if (bucket == null) { 1.289 + bucket = new ArrayList<String>(); 1.290 + } 1.291 + bucket.add(value); 1.292 + map.put(index, bucket); 1.293 + } 1.294 + 1.295 + /** 1.296 + * Yes, an equality method that's null-safe. 1.297 + */ 1.298 + private static boolean same(Object a, Object b) { 1.299 + if (a == b) { 1.300 + return true; 1.301 + } 1.302 + if (a == null || b == null) { 1.303 + return false; // If both null, case above applies. 1.304 + } 1.305 + return a.equals(b); 1.306 + } 1.307 + 1.308 + /** 1.309 + * Return true if the two arrays are both null, or are both arrays 1.310 + * containing the same elements in the same order. 1.311 + */ 1.312 + public static boolean sameArrays(JSONArray a, JSONArray b) { 1.313 + if (a == b) { 1.314 + return true; 1.315 + } 1.316 + if (a == null || b == null) { 1.317 + return false; 1.318 + } 1.319 + final int size = a.size(); 1.320 + if (size != b.size()) { 1.321 + return false; 1.322 + } 1.323 + for (int i = 0; i < size; ++i) { 1.324 + if (!same(a.get(i), b.get(i))) { 1.325 + return false; 1.326 + } 1.327 + } 1.328 + return true; 1.329 + } 1.330 + 1.331 + /** 1.332 + * Takes a URI, extracting URI components. 1.333 + * @param scheme the URI scheme on which to match. 1.334 + */ 1.335 + @SuppressWarnings("deprecation") 1.336 + public static Map<String, String> extractURIComponents(String scheme, String uri) { 1.337 + if (uri.indexOf(scheme) != 0) { 1.338 + throw new IllegalArgumentException("URI scheme does not match: " + scheme); 1.339 + } 1.340 + 1.341 + // Do this the hard way to avoid taking a large dependency on 1.342 + // HttpClient or getting all regex-tastic. 1.343 + String components = uri.substring(scheme.length()); 1.344 + HashMap<String, String> out = new HashMap<String, String>(); 1.345 + String[] parts = components.split("&"); 1.346 + for (int i = 0; i < parts.length; ++i) { 1.347 + String part = parts[i]; 1.348 + if (part.length() == 0) { 1.349 + continue; 1.350 + } 1.351 + String[] pair = part.split("=", 2); 1.352 + switch (pair.length) { 1.353 + case 0: 1.354 + continue; 1.355 + case 1: 1.356 + out.put(URLDecoder.decode(pair[0]), null); 1.357 + break; 1.358 + case 2: 1.359 + out.put(URLDecoder.decode(pair[0]), URLDecoder.decode(pair[1])); 1.360 + break; 1.361 + } 1.362 + } 1.363 + return out; 1.364 + } 1.365 + 1.366 + // Because TextUtils.join is not stubbed. 1.367 + public static String toDelimitedString(String delimiter, Collection<? extends Object> items) { 1.368 + if (items == null || items.size() == 0) { 1.369 + return ""; 1.370 + } 1.371 + 1.372 + StringBuilder sb = new StringBuilder(); 1.373 + int i = 0; 1.374 + int c = items.size(); 1.375 + for (Object object : items) { 1.376 + sb.append(object.toString()); 1.377 + if (++i < c) { 1.378 + sb.append(delimiter); 1.379 + } 1.380 + } 1.381 + return sb.toString(); 1.382 + } 1.383 + 1.384 + public static String toCommaSeparatedString(Collection<? extends Object> items) { 1.385 + return toDelimitedString(", ", items); 1.386 + } 1.387 + 1.388 + /** 1.389 + * Names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP). 1.390 + * 1.391 + * @param knownStageNames collection of known stage names (set ALL above). 1.392 + * @param toSync set SYNC above, or <code>null</code> to sync all known stages. 1.393 + * @param toSkip set SKIP above, or <code>null</code> to not skip any stages. 1.394 + * @return stage names. 1.395 + */ 1.396 + public static Collection<String> getStagesToSync(final Collection<String> knownStageNames, Collection<String> toSync, Collection<String> toSkip) { 1.397 + if (toSkip == null) { 1.398 + toSkip = new HashSet<String>(); 1.399 + } else { 1.400 + toSkip = new HashSet<String>(toSkip); 1.401 + } 1.402 + 1.403 + if (toSync == null) { 1.404 + toSync = new HashSet<String>(knownStageNames); 1.405 + } else { 1.406 + toSync = new HashSet<String>(toSync); 1.407 + } 1.408 + toSync.retainAll(knownStageNames); 1.409 + toSync.removeAll(toSkip); 1.410 + return toSync; 1.411 + } 1.412 + 1.413 + /** 1.414 + * Get names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP). 1.415 + * 1.416 + * @param knownStageNames collection of known stage names (set ALL above). 1.417 + * @param extras 1.418 + * a <code>Bundle</code> instance (possibly null) optionally containing keys 1.419 + * <code>EXTRAS_KEY_STAGES_TO_SYNC</code> (set SYNC above) and 1.420 + * <code>EXTRAS_KEY_STAGES_TO_SKIP</code> (set SKIP above). 1.421 + * @return stage names. 1.422 + */ 1.423 + public static Collection<String> getStagesToSyncFromBundle(final Collection<String> knownStageNames, final Bundle extras) { 1.424 + if (extras == null) { 1.425 + return knownStageNames; 1.426 + } 1.427 + String toSyncString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SYNC); 1.428 + String toSkipString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SKIP); 1.429 + if (toSyncString == null && toSkipString == null) { 1.430 + return knownStageNames; 1.431 + } 1.432 + 1.433 + ArrayList<String> toSync = null; 1.434 + ArrayList<String> toSkip = null; 1.435 + if (toSyncString != null) { 1.436 + try { 1.437 + toSync = new ArrayList<String>(ExtendedJSONObject.parseJSONObject(toSyncString).keySet()); 1.438 + } catch (Exception e) { 1.439 + Logger.warn(LOG_TAG, "Got exception parsing stages to sync: '" + toSyncString + "'.", e); 1.440 + } 1.441 + } 1.442 + if (toSkipString != null) { 1.443 + try { 1.444 + toSkip = new ArrayList<String>(ExtendedJSONObject.parseJSONObject(toSkipString).keySet()); 1.445 + } catch (Exception e) { 1.446 + Logger.warn(LOG_TAG, "Got exception parsing stages to skip: '" + toSkipString + "'.", e); 1.447 + } 1.448 + } 1.449 + 1.450 + Logger.info(LOG_TAG, "Asked to sync '" + Utils.toCommaSeparatedString(toSync) + 1.451 + "' and to skip '" + Utils.toCommaSeparatedString(toSkip) + "'."); 1.452 + return getStagesToSync(knownStageNames, toSync, toSkip); 1.453 + } 1.454 + 1.455 + /** 1.456 + * Put names of stages to sync and to skip into sync extras bundle. 1.457 + * 1.458 + * @param bundle 1.459 + * a <code>Bundle</code> instance (possibly null). 1.460 + * @param stagesToSync 1.461 + * collection of stage names to sync: key 1.462 + * <code>EXTRAS_KEY_STAGES_TO_SYNC</code>; ignored if <code>null</code>. 1.463 + * @param stagesToSkip 1.464 + * collection of stage names to skip: key 1.465 + * <code>EXTRAS_KEY_STAGES_TO_SKIP</code>; ignored if <code>null</code>. 1.466 + */ 1.467 + public static void putStageNamesToSync(final Bundle bundle, final String[] stagesToSync, final String[] stagesToSkip) { 1.468 + if (bundle == null) { 1.469 + return; 1.470 + } 1.471 + 1.472 + if (stagesToSync != null) { 1.473 + ExtendedJSONObject o = new ExtendedJSONObject(); 1.474 + for (String stageName : stagesToSync) { 1.475 + o.put(stageName, 0); 1.476 + } 1.477 + bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SYNC, o.toJSONString()); 1.478 + } 1.479 + 1.480 + if (stagesToSkip != null) { 1.481 + ExtendedJSONObject o = new ExtendedJSONObject(); 1.482 + for (String stageName : stagesToSkip) { 1.483 + o.put(stageName, 0); 1.484 + } 1.485 + bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SKIP, o.toJSONString()); 1.486 + } 1.487 + } 1.488 + 1.489 + /** 1.490 + * Read contents of file as a string. 1.491 + * 1.492 + * @param context Android context. 1.493 + * @param filename name of file to read; must not be null. 1.494 + * @return <code>String</code> instance. 1.495 + */ 1.496 + public static String readFile(final Context context, final String filename) { 1.497 + if (filename == null) { 1.498 + throw new IllegalArgumentException("Passed null filename in readFile."); 1.499 + } 1.500 + 1.501 + FileInputStream fis = null; 1.502 + InputStreamReader isr = null; 1.503 + BufferedReader br = null; 1.504 + 1.505 + try { 1.506 + fis = context.openFileInput(filename); 1.507 + isr = new InputStreamReader(fis); 1.508 + br = new BufferedReader(isr); 1.509 + StringBuilder sb = new StringBuilder(); 1.510 + String line; 1.511 + while ((line = br.readLine()) != null) { 1.512 + sb.append(line); 1.513 + } 1.514 + return sb.toString(); 1.515 + } catch (Exception e) { 1.516 + return null; 1.517 + } finally { 1.518 + if (isr != null) { 1.519 + try { 1.520 + isr.close(); 1.521 + } catch (IOException e) { 1.522 + // Ignore. 1.523 + } 1.524 + } 1.525 + if (fis != null) { 1.526 + try { 1.527 + fis.close(); 1.528 + } catch (IOException e) { 1.529 + // Ignore. 1.530 + } 1.531 + } 1.532 + } 1.533 + } 1.534 + 1.535 + /** 1.536 + * Format a duration as a string, like "0.56 seconds". 1.537 + * 1.538 + * @param startMillis start time in milliseconds. 1.539 + * @param endMillis end time in milliseconds. 1.540 + * @return formatted string. 1.541 + */ 1.542 + public static String formatDuration(long startMillis, long endMillis) { 1.543 + final long duration = endMillis - startMillis; 1.544 + return new DecimalFormat("#0.00 seconds").format(((double) duration) / 1000); 1.545 + } 1.546 + 1.547 + /** 1.548 + * This will take a string containing a UTF-8 representation of a UTF-8 1.549 + * byte array — e.g., "pïgéons1" — and return UTF-8 (e.g., "pïgéons1"). 1.550 + * 1.551 + * This is the format produced by desktop Firefox when exchanging credentials 1.552 + * containing non-ASCII characters. 1.553 + */ 1.554 + public static String decodeUTF8(final String in) throws UnsupportedEncodingException { 1.555 + final int length = in.length(); 1.556 + final byte[] asciiBytes = new byte[length]; 1.557 + for (int i = 0; i < length; ++i) { 1.558 + asciiBytes[i] = (byte) in.codePointAt(i); 1.559 + } 1.560 + return new String(asciiBytes, "UTF-8"); 1.561 + } 1.562 + 1.563 + /** 1.564 + * Replace "foo@bar.com" with "XXX@XXX.XXX". 1.565 + */ 1.566 + public static String obfuscateEmail(final String in) { 1.567 + return in.replaceAll("[^@\\.]", "X"); 1.568 + } 1.569 + 1.570 + public static String nodeWeaveURL(String serverURL, String username) { 1.571 + String userPart = username + "/node/weave"; 1.572 + if (serverURL == null) { 1.573 + return SyncConstants.DEFAULT_AUTH_SERVER + "user/1.0/" + userPart; 1.574 + } 1.575 + if (!serverURL.endsWith("/")) { 1.576 + serverURL = serverURL + "/"; 1.577 + } 1.578 + return serverURL + "user/1.0/" + userPart; 1.579 + } 1.580 + 1.581 + public static void throwIfNull(Object... objects) { 1.582 + for (Object object : objects) { 1.583 + if (object == null) { 1.584 + throw new IllegalArgumentException("object must not be null"); 1.585 + } 1.586 + } 1.587 + } 1.588 + 1.589 + /** 1.590 + * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale} 1.591 + * stringifies as "es_ES". 1.592 + * 1.593 + * This method approximates the Java 7 method <code>Locale#toLanguageTag()</code>. 1.594 + * <p> 1.595 + * <b>Warning:</b> all consumers of this method will need to be audited when 1.596 + * we have active locale switching. 1.597 + * 1.598 + * @return a locale string suitable for passing to Gecko. 1.599 + */ 1.600 + public static String getLanguageTag(final Locale locale) { 1.601 + // If this were Java 7: 1.602 + // return locale.toLanguageTag(); 1.603 + 1.604 + String language = locale.getLanguage(); // Can, but should never be, an empty string. 1.605 + // Modernize certain language codes. 1.606 + if (language.equals("iw")) { 1.607 + language = "he"; 1.608 + } else if (language.equals("in")) { 1.609 + language = "id"; 1.610 + } else if (language.equals("ji")) { 1.611 + language = "yi"; 1.612 + } 1.613 + 1.614 + String country = locale.getCountry(); // Can be an empty string. 1.615 + if (country.equals("")) { 1.616 + return language; 1.617 + } 1.618 + return language + "-" + country; 1.619 + } 1.620 +}