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.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