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