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