1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.keyboard.internal; 18 19 import static com.android.inputmethod.latin.Constants.CODE_OUTPUT_TEXT; 20 import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED; 21 22 import android.text.TextUtils; 23 24 import com.android.inputmethod.latin.Constants; 25 import com.android.inputmethod.latin.LatinImeLogger; 26 import com.android.inputmethod.latin.utils.CollectionUtils; 27 import com.android.inputmethod.latin.utils.StringUtils; 28 29 import java.util.ArrayList; 30 import java.util.Arrays; 31 import java.util.Locale; 32 33 /** 34 * The string parser of more keys specification. 35 * The specification is comma separated texts each of which represents one "more key". 36 * The specification might have label or string resource reference in it. These references are 37 * expanded before parsing comma. 38 * - Label reference should be a string representation of label (!text/label_name) 39 * - String resource reference should be a string representation of resource (!text/resource_name) 40 * Each "more key" specification is one of the following: 41 * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText). 42 * - Icon followed by keyOutputText or code (!icon/icon_name|!code/code_name) 43 * - Icon should be a string representation of icon (!icon/icon_name). 44 * - Code should be a code point presented by hexadecimal string prefixed with "0x", or a string 45 * representation of code (!code/code_name). 46 * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character. 47 * Note that the '\' is also parsed by XML parser and CSV parser as well. 48 * See {@link KeyboardIconsSet} about icon_name. 49 */ 50 public final class KeySpecParser { 51 private static final boolean DEBUG = LatinImeLogger.sDBG; 52 53 private static final int MAX_STRING_REFERENCE_INDIRECTION = 10; 54 55 // Constants for parsing. 56 private static final char COMMA = ','; 57 private static final char BACKSLASH = '\\'; 58 private static final char VERTICAL_BAR = '|'; 59 private static final String PREFIX_TEXT = "!text/"; 60 static final String PREFIX_ICON = "!icon/"; 61 private static final String PREFIX_CODE = "!code/"; 62 private static final String PREFIX_HEX = "0x"; 63 private static final String ADDITIONAL_MORE_KEY_MARKER = "%"; 64 65 private KeySpecParser() { 66 // Intentional empty constructor for utility class. 67 } 68 69 /** 70 * Split the text containing multiple key specifications separated by commas into an array of 71 * key specifications. 72 * A key specification can contain a character escaped by the backslash character, including a 73 * comma character. 74 * Note that an empty key specification will be eliminated from the result array. 75 * 76 * @param text the text containing multiple key specifications. 77 * @return an array of key specification text. Null if the specified <code>text</code> is empty 78 * or has no key specifications. 79 */ 80 public static String[] splitKeySpecs(final String text) { 81 final int size = text.length(); 82 if (size == 0) { 83 return null; 84 } 85 // Optimization for one-letter key specification. 86 if (size == 1) { 87 return text.charAt(0) == COMMA ? null : new String[] { text }; 88 } 89 90 ArrayList<String> list = null; 91 int start = 0; 92 // The characters in question in this loop are COMMA and BACKSLASH. These characters never 93 // match any high or low surrogate character. So it is OK to iterate through with char 94 // index. 95 for (int pos = 0; pos < size; pos++) { 96 final char c = text.charAt(pos); 97 if (c == COMMA) { 98 // Skip empty entry. 99 if (pos - start > 0) { 100 if (list == null) { 101 list = CollectionUtils.newArrayList(); 102 } 103 list.add(text.substring(start, pos)); 104 } 105 // Skip comma 106 start = pos + 1; 107 } else if (c == BACKSLASH) { 108 // Skip escape character and escaped character. 109 pos++; 110 } 111 } 112 final String remain = (size - start > 0) ? text.substring(start) : null; 113 if (list == null) { 114 return remain != null ? new String[] { remain } : null; 115 } 116 if (remain != null) { 117 list.add(remain); 118 } 119 return list.toArray(new String[list.size()]); 120 } 121 122 private static boolean hasIcon(final String moreKeySpec) { 123 return moreKeySpec.startsWith(PREFIX_ICON); 124 } 125 126 private static boolean hasCode(final String moreKeySpec) { 127 final int end = indexOfLabelEnd(moreKeySpec, 0); 128 if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith( 129 PREFIX_CODE, end + 1)) { 130 return true; 131 } 132 return false; 133 } 134 135 private static String parseEscape(final String text) { 136 if (text.indexOf(BACKSLASH) < 0) { 137 return text; 138 } 139 final int length = text.length(); 140 final StringBuilder sb = new StringBuilder(); 141 for (int pos = 0; pos < length; pos++) { 142 final char c = text.charAt(pos); 143 if (c == BACKSLASH && pos + 1 < length) { 144 // Skip escape char 145 pos++; 146 sb.append(text.charAt(pos)); 147 } else { 148 sb.append(c); 149 } 150 } 151 return sb.toString(); 152 } 153 154 private static int indexOfLabelEnd(final String moreKeySpec, final int start) { 155 if (moreKeySpec.indexOf(BACKSLASH, start) < 0) { 156 final int end = moreKeySpec.indexOf(VERTICAL_BAR, start); 157 if (end == 0) { 158 throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + moreKeySpec); 159 } 160 return end; 161 } 162 final int length = moreKeySpec.length(); 163 for (int pos = start; pos < length; pos++) { 164 final char c = moreKeySpec.charAt(pos); 165 if (c == BACKSLASH && pos + 1 < length) { 166 // Skip escape char 167 pos++; 168 } else if (c == VERTICAL_BAR) { 169 return pos; 170 } 171 } 172 return -1; 173 } 174 175 public static String getLabel(final String moreKeySpec) { 176 if (hasIcon(moreKeySpec)) { 177 return null; 178 } 179 final int end = indexOfLabelEnd(moreKeySpec, 0); 180 final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end)) 181 : parseEscape(moreKeySpec); 182 if (TextUtils.isEmpty(label)) { 183 throw new KeySpecParserError("Empty label: " + moreKeySpec); 184 } 185 return label; 186 } 187 188 private static String getOutputTextInternal(final String moreKeySpec) { 189 final int end = indexOfLabelEnd(moreKeySpec, 0); 190 if (end <= 0) { 191 return null; 192 } 193 if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { 194 throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec); 195 } 196 return parseEscape(moreKeySpec.substring(end + /* VERTICAL_BAR */1)); 197 } 198 199 static String getOutputText(final String moreKeySpec) { 200 if (hasCode(moreKeySpec)) { 201 return null; 202 } 203 final String outputText = getOutputTextInternal(moreKeySpec); 204 if (outputText != null) { 205 if (StringUtils.codePointCount(outputText) == 1) { 206 // If output text is one code point, it should be treated as a code. 207 // See {@link #getCode(Resources, String)}. 208 return null; 209 } 210 if (!TextUtils.isEmpty(outputText)) { 211 return outputText; 212 } 213 throw new KeySpecParserError("Empty outputText: " + moreKeySpec); 214 } 215 final String label = getLabel(moreKeySpec); 216 if (label == null) { 217 throw new KeySpecParserError("Empty label: " + moreKeySpec); 218 } 219 // Code is automatically generated for one letter label. See {@link getCode()}. 220 return (StringUtils.codePointCount(label) == 1) ? null : label; 221 } 222 223 static int getCode(final String moreKeySpec, final KeyboardCodesSet codesSet) { 224 if (hasCode(moreKeySpec)) { 225 final int end = indexOfLabelEnd(moreKeySpec, 0); 226 if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { 227 throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec); 228 } 229 return parseCode(moreKeySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED); 230 } 231 final String outputText = getOutputTextInternal(moreKeySpec); 232 if (outputText != null) { 233 // If output text is one code point, it should be treated as a code. 234 // See {@link #getOutputText(String)}. 235 if (StringUtils.codePointCount(outputText) == 1) { 236 return outputText.codePointAt(0); 237 } 238 return CODE_OUTPUT_TEXT; 239 } 240 final String label = getLabel(moreKeySpec); 241 // Code is automatically generated for one letter label. 242 if (StringUtils.codePointCount(label) == 1) { 243 return label.codePointAt(0); 244 } 245 return CODE_OUTPUT_TEXT; 246 } 247 248 public static int parseCode(final String text, final KeyboardCodesSet codesSet, 249 final int defCode) { 250 if (text == null) return defCode; 251 if (text.startsWith(PREFIX_CODE)) { 252 return codesSet.getCode(text.substring(PREFIX_CODE.length())); 253 } else if (text.startsWith(PREFIX_HEX)) { 254 return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16); 255 } else { 256 return Integer.parseInt(text); 257 } 258 } 259 260 public static int getIconId(final String moreKeySpec) { 261 if (moreKeySpec != null && hasIcon(moreKeySpec)) { 262 final int end = moreKeySpec.indexOf(VERTICAL_BAR, PREFIX_ICON.length()); 263 final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length()) 264 : moreKeySpec.substring(PREFIX_ICON.length(), end); 265 return KeyboardIconsSet.getIconId(name); 266 } 267 return KeyboardIconsSet.ICON_UNDEFINED; 268 } 269 270 private static <T> ArrayList<T> arrayAsList(final T[] array, final int start, final int end) { 271 if (array == null) { 272 throw new NullPointerException(); 273 } 274 if (start < 0 || start > end || end > array.length) { 275 throw new IllegalArgumentException(); 276 } 277 278 final ArrayList<T> list = CollectionUtils.newArrayList(end - start); 279 for (int i = start; i < end; i++) { 280 list.add(array[i]); 281 } 282 return list; 283 } 284 285 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 286 287 private static String[] filterOutEmptyString(final String[] array) { 288 if (array == null) { 289 return EMPTY_STRING_ARRAY; 290 } 291 ArrayList<String> out = null; 292 for (int i = 0; i < array.length; i++) { 293 final String entry = array[i]; 294 if (TextUtils.isEmpty(entry)) { 295 if (out == null) { 296 out = arrayAsList(array, 0, i); 297 } 298 } else if (out != null) { 299 out.add(entry); 300 } 301 } 302 if (out == null) { 303 return array; 304 } 305 return out.toArray(new String[out.size()]); 306 } 307 308 public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs, 309 final String[] additionalMoreKeySpecs) { 310 final String[] moreKeys = filterOutEmptyString(moreKeySpecs); 311 final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs); 312 final int moreKeysCount = moreKeys.length; 313 final int additionalCount = additionalMoreKeys.length; 314 ArrayList<String> out = null; 315 int additionalIndex = 0; 316 for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) { 317 final String moreKeySpec = moreKeys[moreKeyIndex]; 318 if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) { 319 if (additionalIndex < additionalCount) { 320 // Replace '%' marker with additional more key specification. 321 final String additionalMoreKey = additionalMoreKeys[additionalIndex]; 322 if (out != null) { 323 out.add(additionalMoreKey); 324 } else { 325 moreKeys[moreKeyIndex] = additionalMoreKey; 326 } 327 additionalIndex++; 328 } else { 329 // Filter out excessive '%' marker. 330 if (out == null) { 331 out = arrayAsList(moreKeys, 0, moreKeyIndex); 332 } 333 } 334 } else { 335 if (out != null) { 336 out.add(moreKeySpec); 337 } 338 } 339 } 340 if (additionalCount > 0 && additionalIndex == 0) { 341 // No '%' marker is found in more keys. 342 // Insert all additional more keys to the head of more keys. 343 if (DEBUG && out != null) { 344 throw new RuntimeException("Internal logic error:" 345 + " moreKeys=" + Arrays.toString(moreKeys) 346 + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); 347 } 348 out = arrayAsList(additionalMoreKeys, additionalIndex, additionalCount); 349 for (int i = 0; i < moreKeysCount; i++) { 350 out.add(moreKeys[i]); 351 } 352 } else if (additionalIndex < additionalCount) { 353 // The number of '%' markers are less than additional more keys. 354 // Append remained additional more keys to the tail of more keys. 355 if (DEBUG && out != null) { 356 throw new RuntimeException("Internal logic error:" 357 + " moreKeys=" + Arrays.toString(moreKeys) 358 + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); 359 } 360 out = arrayAsList(moreKeys, 0, moreKeysCount); 361 for (int i = additionalIndex; i < additionalCount; i++) { 362 out.add(additionalMoreKeys[additionalIndex]); 363 } 364 } 365 if (out == null && moreKeysCount > 0) { 366 return moreKeys; 367 } else if (out != null && out.size() > 0) { 368 return out.toArray(new String[out.size()]); 369 } else { 370 return null; 371 } 372 } 373 374 @SuppressWarnings("serial") 375 public static final class KeySpecParserError extends RuntimeException { 376 public KeySpecParserError(final String message) { 377 super(message); 378 } 379 } 380 381 public static String resolveTextReference(final String rawText, 382 final KeyboardTextsSet textsSet) { 383 int level = 0; 384 String text = rawText; 385 StringBuilder sb; 386 do { 387 level++; 388 if (level >= MAX_STRING_REFERENCE_INDIRECTION) { 389 throw new RuntimeException("too many @string/resource indirection: " + text); 390 } 391 392 final int prefixLen = PREFIX_TEXT.length(); 393 final int size = text.length(); 394 if (size < prefixLen) { 395 return text; 396 } 397 398 sb = null; 399 for (int pos = 0; pos < size; pos++) { 400 final char c = text.charAt(pos); 401 if (text.startsWith(PREFIX_TEXT, pos) && textsSet != null) { 402 if (sb == null) { 403 sb = new StringBuilder(text.substring(0, pos)); 404 } 405 final int end = searchTextNameEnd(text, pos + prefixLen); 406 final String name = text.substring(pos + prefixLen, end); 407 sb.append(textsSet.getText(name)); 408 pos = end - 1; 409 } else if (c == BACKSLASH) { 410 if (sb != null) { 411 // Append both escape character and escaped character. 412 sb.append(text.substring(pos, Math.min(pos + 2, size))); 413 } 414 pos++; 415 } else if (sb != null) { 416 sb.append(c); 417 } 418 } 419 420 if (sb != null) { 421 text = sb.toString(); 422 } 423 } while (sb != null); 424 return text; 425 } 426 427 private static int searchTextNameEnd(final String text, final int start) { 428 final int size = text.length(); 429 for (int pos = start; pos < size; pos++) { 430 final char c = text.charAt(pos); 431 // Label name should be consisted of [a-zA-Z_0-9]. 432 if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) { 433 continue; 434 } 435 return pos; 436 } 437 return size; 438 } 439 440 public static int getIntValue(final String[] moreKeys, final String key, 441 final int defaultValue) { 442 if (moreKeys == null) { 443 return defaultValue; 444 } 445 final int keyLen = key.length(); 446 boolean foundValue = false; 447 int value = defaultValue; 448 for (int i = 0; i < moreKeys.length; i++) { 449 final String moreKeySpec = moreKeys[i]; 450 if (moreKeySpec == null || !moreKeySpec.startsWith(key)) { 451 continue; 452 } 453 moreKeys[i] = null; 454 try { 455 if (!foundValue) { 456 value = Integer.parseInt(moreKeySpec.substring(keyLen)); 457 foundValue = true; 458 } 459 } catch (NumberFormatException e) { 460 throw new RuntimeException( 461 "integer should follow after " + key + ": " + moreKeySpec); 462 } 463 } 464 return value; 465 } 466 467 public static boolean getBooleanValue(final String[] moreKeys, final String key) { 468 if (moreKeys == null) { 469 return false; 470 } 471 boolean value = false; 472 for (int i = 0; i < moreKeys.length; i++) { 473 final String moreKeySpec = moreKeys[i]; 474 if (moreKeySpec == null || !moreKeySpec.equals(key)) { 475 continue; 476 } 477 moreKeys[i] = null; 478 value = true; 479 } 480 return value; 481 } 482 483 public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase, 484 final Locale locale) { 485 if (!Constants.isLetterCode(code) || !needsToUpperCase) return code; 486 final String text = new String(new int[] { code } , 0, 1); 487 final String casedText = KeySpecParser.toUpperCaseOfStringForLocale( 488 text, needsToUpperCase, locale); 489 return StringUtils.codePointCount(casedText) == 1 490 ? casedText.codePointAt(0) : CODE_UNSPECIFIED; 491 } 492 493 public static String toUpperCaseOfStringForLocale(final String text, 494 final boolean needsToUpperCase, final Locale locale) { 495 if (text == null || !needsToUpperCase) return text; 496 return text.toUpperCase(locale); 497 } 498 } 499