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.CollectionUtils; 25 import com.android.inputmethod.latin.Constants; 26 import com.android.inputmethod.latin.LatinImeLogger; 27 import com.android.inputmethod.latin.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 LABEL_END = '|'; 57 private static final String PREFIX_TEXT = "!text/"; 58 static final String PREFIX_ICON = "!icon/"; 59 private static final String PREFIX_CODE = "!code/"; 60 private static final String PREFIX_HEX = "0x"; 61 private static final String ADDITIONAL_MORE_KEY_MARKER = "%"; 62 63 private KeySpecParser() { 64 // Intentional empty constructor for utility class. 65 } 66 67 private static boolean hasIcon(final String moreKeySpec) { 68 return moreKeySpec.startsWith(PREFIX_ICON); 69 } 70 71 private static boolean hasCode(final String moreKeySpec) { 72 final int end = indexOfLabelEnd(moreKeySpec, 0); 73 if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith( 74 PREFIX_CODE, end + 1)) { 75 return true; 76 } 77 return false; 78 } 79 80 private static String parseEscape(final String text) { 81 if (text.indexOf(Constants.CSV_ESCAPE) < 0) { 82 return text; 83 } 84 final int length = text.length(); 85 final StringBuilder sb = new StringBuilder(); 86 for (int pos = 0; pos < length; pos++) { 87 final char c = text.charAt(pos); 88 if (c == Constants.CSV_ESCAPE && pos + 1 < length) { 89 // Skip escape char 90 pos++; 91 sb.append(text.charAt(pos)); 92 } else { 93 sb.append(c); 94 } 95 } 96 return sb.toString(); 97 } 98 99 private static int indexOfLabelEnd(final String moreKeySpec, final int start) { 100 if (moreKeySpec.indexOf(Constants.CSV_ESCAPE, start) < 0) { 101 final int end = moreKeySpec.indexOf(LABEL_END, start); 102 if (end == 0) { 103 throw new KeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec); 104 } 105 return end; 106 } 107 final int length = moreKeySpec.length(); 108 for (int pos = start; pos < length; pos++) { 109 final char c = moreKeySpec.charAt(pos); 110 if (c == Constants.CSV_ESCAPE && pos + 1 < length) { 111 // Skip escape char 112 pos++; 113 } else if (c == LABEL_END) { 114 return pos; 115 } 116 } 117 return -1; 118 } 119 120 public static String getLabel(final String moreKeySpec) { 121 if (hasIcon(moreKeySpec)) { 122 return null; 123 } 124 final int end = indexOfLabelEnd(moreKeySpec, 0); 125 final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end)) 126 : parseEscape(moreKeySpec); 127 if (TextUtils.isEmpty(label)) { 128 throw new KeySpecParserError("Empty label: " + moreKeySpec); 129 } 130 return label; 131 } 132 133 private static String getOutputTextInternal(final String moreKeySpec) { 134 final int end = indexOfLabelEnd(moreKeySpec, 0); 135 if (end <= 0) { 136 return null; 137 } 138 if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { 139 throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec); 140 } 141 return parseEscape(moreKeySpec.substring(end + /* LABEL_END */1)); 142 } 143 144 static String getOutputText(final String moreKeySpec) { 145 if (hasCode(moreKeySpec)) { 146 return null; 147 } 148 final String outputText = getOutputTextInternal(moreKeySpec); 149 if (outputText != null) { 150 if (StringUtils.codePointCount(outputText) == 1) { 151 // If output text is one code point, it should be treated as a code. 152 // See {@link #getCode(Resources, String)}. 153 return null; 154 } 155 if (!TextUtils.isEmpty(outputText)) { 156 return outputText; 157 } 158 throw new KeySpecParserError("Empty outputText: " + moreKeySpec); 159 } 160 final String label = getLabel(moreKeySpec); 161 if (label == null) { 162 throw new KeySpecParserError("Empty label: " + moreKeySpec); 163 } 164 // Code is automatically generated for one letter label. See {@link getCode()}. 165 return (StringUtils.codePointCount(label) == 1) ? null : label; 166 } 167 168 static int getCode(final String moreKeySpec, final KeyboardCodesSet codesSet) { 169 if (hasCode(moreKeySpec)) { 170 final int end = indexOfLabelEnd(moreKeySpec, 0); 171 if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { 172 throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec); 173 } 174 return parseCode(moreKeySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED); 175 } 176 final String outputText = getOutputTextInternal(moreKeySpec); 177 if (outputText != null) { 178 // If output text is one code point, it should be treated as a code. 179 // See {@link #getOutputText(String)}. 180 if (StringUtils.codePointCount(outputText) == 1) { 181 return outputText.codePointAt(0); 182 } 183 return CODE_OUTPUT_TEXT; 184 } 185 final String label = getLabel(moreKeySpec); 186 // Code is automatically generated for one letter label. 187 if (StringUtils.codePointCount(label) == 1) { 188 return label.codePointAt(0); 189 } 190 return CODE_OUTPUT_TEXT; 191 } 192 193 public static int parseCode(final String text, final KeyboardCodesSet codesSet, 194 final int defCode) { 195 if (text == null) return defCode; 196 if (text.startsWith(PREFIX_CODE)) { 197 return codesSet.getCode(text.substring(PREFIX_CODE.length())); 198 } else if (text.startsWith(PREFIX_HEX)) { 199 return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16); 200 } else { 201 return Integer.parseInt(text); 202 } 203 } 204 205 public static int getIconId(final String moreKeySpec) { 206 if (moreKeySpec != null && hasIcon(moreKeySpec)) { 207 final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length()); 208 final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length()) 209 : moreKeySpec.substring(PREFIX_ICON.length(), end); 210 return KeyboardIconsSet.getIconId(name); 211 } 212 return KeyboardIconsSet.ICON_UNDEFINED; 213 } 214 215 private static <T> ArrayList<T> arrayAsList(final T[] array, final int start, final int end) { 216 if (array == null) { 217 throw new NullPointerException(); 218 } 219 if (start < 0 || start > end || end > array.length) { 220 throw new IllegalArgumentException(); 221 } 222 223 final ArrayList<T> list = CollectionUtils.newArrayList(end - start); 224 for (int i = start; i < end; i++) { 225 list.add(array[i]); 226 } 227 return list; 228 } 229 230 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 231 232 private static String[] filterOutEmptyString(final String[] array) { 233 if (array == null) { 234 return EMPTY_STRING_ARRAY; 235 } 236 ArrayList<String> out = null; 237 for (int i = 0; i < array.length; i++) { 238 final String entry = array[i]; 239 if (TextUtils.isEmpty(entry)) { 240 if (out == null) { 241 out = arrayAsList(array, 0, i); 242 } 243 } else if (out != null) { 244 out.add(entry); 245 } 246 } 247 if (out == null) { 248 return array; 249 } 250 return out.toArray(new String[out.size()]); 251 } 252 253 public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs, 254 final String[] additionalMoreKeySpecs) { 255 final String[] moreKeys = filterOutEmptyString(moreKeySpecs); 256 final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs); 257 final int moreKeysCount = moreKeys.length; 258 final int additionalCount = additionalMoreKeys.length; 259 ArrayList<String> out = null; 260 int additionalIndex = 0; 261 for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) { 262 final String moreKeySpec = moreKeys[moreKeyIndex]; 263 if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) { 264 if (additionalIndex < additionalCount) { 265 // Replace '%' marker with additional more key specification. 266 final String additionalMoreKey = additionalMoreKeys[additionalIndex]; 267 if (out != null) { 268 out.add(additionalMoreKey); 269 } else { 270 moreKeys[moreKeyIndex] = additionalMoreKey; 271 } 272 additionalIndex++; 273 } else { 274 // Filter out excessive '%' marker. 275 if (out == null) { 276 out = arrayAsList(moreKeys, 0, moreKeyIndex); 277 } 278 } 279 } else { 280 if (out != null) { 281 out.add(moreKeySpec); 282 } 283 } 284 } 285 if (additionalCount > 0 && additionalIndex == 0) { 286 // No '%' marker is found in more keys. 287 // Insert all additional more keys to the head of more keys. 288 if (DEBUG && out != null) { 289 throw new RuntimeException("Internal logic error:" 290 + " moreKeys=" + Arrays.toString(moreKeys) 291 + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); 292 } 293 out = arrayAsList(additionalMoreKeys, additionalIndex, additionalCount); 294 for (int i = 0; i < moreKeysCount; i++) { 295 out.add(moreKeys[i]); 296 } 297 } else if (additionalIndex < additionalCount) { 298 // The number of '%' markers are less than additional more keys. 299 // Append remained additional more keys to the tail of more keys. 300 if (DEBUG && out != null) { 301 throw new RuntimeException("Internal logic error:" 302 + " moreKeys=" + Arrays.toString(moreKeys) 303 + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); 304 } 305 out = arrayAsList(moreKeys, 0, moreKeysCount); 306 for (int i = additionalIndex; i < additionalCount; i++) { 307 out.add(additionalMoreKeys[additionalIndex]); 308 } 309 } 310 if (out == null && moreKeysCount > 0) { 311 return moreKeys; 312 } else if (out != null && out.size() > 0) { 313 return out.toArray(new String[out.size()]); 314 } else { 315 return null; 316 } 317 } 318 319 @SuppressWarnings("serial") 320 public static final class KeySpecParserError extends RuntimeException { 321 public KeySpecParserError(final String message) { 322 super(message); 323 } 324 } 325 326 public static String resolveTextReference(final String rawText, 327 final KeyboardTextsSet textsSet) { 328 int level = 0; 329 String text = rawText; 330 StringBuilder sb; 331 do { 332 level++; 333 if (level >= MAX_STRING_REFERENCE_INDIRECTION) { 334 throw new RuntimeException("too many @string/resource indirection: " + text); 335 } 336 337 final int prefixLen = PREFIX_TEXT.length(); 338 final int size = text.length(); 339 if (size < prefixLen) { 340 return text; 341 } 342 343 sb = null; 344 for (int pos = 0; pos < size; pos++) { 345 final char c = text.charAt(pos); 346 if (text.startsWith(PREFIX_TEXT, pos) && textsSet != null) { 347 if (sb == null) { 348 sb = new StringBuilder(text.substring(0, pos)); 349 } 350 final int end = searchTextNameEnd(text, pos + prefixLen); 351 final String name = text.substring(pos + prefixLen, end); 352 sb.append(textsSet.getText(name)); 353 pos = end - 1; 354 } else if (c == Constants.CSV_ESCAPE) { 355 if (sb != null) { 356 // Append both escape character and escaped character. 357 sb.append(text.substring(pos, Math.min(pos + 2, size))); 358 } 359 pos++; 360 } else if (sb != null) { 361 sb.append(c); 362 } 363 } 364 365 if (sb != null) { 366 text = sb.toString(); 367 } 368 } while (sb != null); 369 370 return text; 371 } 372 373 private static int searchTextNameEnd(final String text, final int start) { 374 final int size = text.length(); 375 for (int pos = start; pos < size; pos++) { 376 final char c = text.charAt(pos); 377 // Label name should be consisted of [a-zA-Z_0-9]. 378 if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) { 379 continue; 380 } 381 return pos; 382 } 383 return size; 384 } 385 386 public static int getIntValue(final String[] moreKeys, final String key, 387 final int defaultValue) { 388 if (moreKeys == null) { 389 return defaultValue; 390 } 391 final int keyLen = key.length(); 392 boolean foundValue = false; 393 int value = defaultValue; 394 for (int i = 0; i < moreKeys.length; i++) { 395 final String moreKeySpec = moreKeys[i]; 396 if (moreKeySpec == null || !moreKeySpec.startsWith(key)) { 397 continue; 398 } 399 moreKeys[i] = null; 400 try { 401 if (!foundValue) { 402 value = Integer.parseInt(moreKeySpec.substring(keyLen)); 403 foundValue = true; 404 } 405 } catch (NumberFormatException e) { 406 throw new RuntimeException( 407 "integer should follow after " + key + ": " + moreKeySpec); 408 } 409 } 410 return value; 411 } 412 413 public static boolean getBooleanValue(final String[] moreKeys, final String key) { 414 if (moreKeys == null) { 415 return false; 416 } 417 boolean value = false; 418 for (int i = 0; i < moreKeys.length; i++) { 419 final String moreKeySpec = moreKeys[i]; 420 if (moreKeySpec == null || !moreKeySpec.equals(key)) { 421 continue; 422 } 423 moreKeys[i] = null; 424 value = true; 425 } 426 return value; 427 } 428 429 public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase, 430 final Locale locale) { 431 if (!Constants.isLetterCode(code) || !needsToUpperCase) return code; 432 final String text = new String(new int[] { code } , 0, 1); 433 final String casedText = KeySpecParser.toUpperCaseOfStringForLocale( 434 text, needsToUpperCase, locale); 435 return StringUtils.codePointCount(casedText) == 1 436 ? casedText.codePointAt(0) : CODE_UNSPECIFIED; 437 } 438 439 public static String toUpperCaseOfStringForLocale(final String text, 440 final boolean needsToUpperCase, final Locale locale) { 441 if (text == null || !needsToUpperCase) return text; 442 return text.toUpperCase(locale); 443 } 444 } 445