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");
      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