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