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