KeySpecParser.java revision d9c6b332090c90e4d4840e62fe3eb45c834b2e14
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
17package com.android.inputmethod.keyboard.internal;
18
19import static com.android.inputmethod.latin.Constants.CODE_OUTPUT_TEXT;
20import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED;
21
22import com.android.inputmethod.latin.Constants;
23import com.android.inputmethod.latin.utils.StringUtils;
24
25/**
26 * The string parser of the key specification.
27 *
28 * Each key specification is one of the following:
29 * - Label optionally followed by keyOutputText (keyLabel|keyOutputText).
30 * - Label optionally followed by code point (keyLabel|!code/code_name).
31 * - Icon followed by keyOutputText (!icon/icon_name|keyOutputText).
32 * - Icon followed by code point (!icon/icon_name|!code/code_name).
33 * Label and keyOutputText are one of the following:
34 * - Literal string.
35 * - Label reference represented by (!text/label_name), see {@link KeyboardTextsSet}.
36 * - String resource reference represented by (!text/resource_name), see {@link KeyboardTextsSet}.
37 * Icon is represented by (!icon/icon_name), see {@link KeyboardIconsSet}.
38 * Code is one of the following:
39 * - Code point presented by hexadecimal string prefixed with "0x"
40 * - Code reference represented by (!code/code_name), see {@link KeyboardCodesSet}.
41 * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character.
42 * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
43 * as well.
44 */
45public final class KeySpecParser {
46    // Constants for parsing.
47    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
48    private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR;
49    private static final String PREFIX_HEX = "0x";
50
51    private KeySpecParser() {
52        // Intentional empty constructor for utility class.
53    }
54
55    private static boolean hasIcon(final String keySpec) {
56        return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
57    }
58
59    private static boolean hasCode(final String keySpec, final int labelEnd) {
60        if (labelEnd > 0 && labelEnd + 1 < keySpec.length()
61                && keySpec.startsWith(KeyboardCodesSet.PREFIX_CODE, labelEnd + 1)) {
62            return true;
63        }
64        return false;
65    }
66
67    private static String parseEscape(final String text) {
68        if (text.indexOf(BACKSLASH) < 0) {
69            return text;
70        }
71        final int length = text.length();
72        final StringBuilder sb = new StringBuilder();
73        for (int pos = 0; pos < length; pos++) {
74            final char c = text.charAt(pos);
75            if (c == BACKSLASH && pos + 1 < length) {
76                // Skip escape char
77                pos++;
78                sb.append(text.charAt(pos));
79            } else {
80                sb.append(c);
81            }
82        }
83        return sb.toString();
84    }
85
86    private static int indexOfLabelEnd(final String keySpec) {
87        if (keySpec.indexOf(BACKSLASH) < 0) {
88            final int labelEnd = keySpec.indexOf(VERTICAL_BAR);
89            if (labelEnd == 0) {
90                throw new KeySpecParserError("Empty label");
91            }
92            return labelEnd;
93        }
94        final int length = keySpec.length();
95        for (int pos = 0; pos < length; pos++) {
96            final char c = keySpec.charAt(pos);
97            if (c == BACKSLASH && pos + 1 < length) {
98                // Skip escape char
99                pos++;
100            } else if (c == VERTICAL_BAR) {
101                return pos;
102            }
103        }
104        return -1;
105    }
106
107    private static String getBeforeLabelEnd(final String keySpec, final int labelEnd) {
108        return (labelEnd < 0) ? keySpec : keySpec.substring(0, labelEnd);
109    }
110
111    private static String getAfterLabelEnd(final String keySpec, final int labelEnd) {
112        return keySpec.substring(labelEnd + /* VERTICAL_BAR */1);
113    }
114
115    private static void checkDoubleLabelEnd(final String keySpec, final int labelEnd) {
116        if (indexOfLabelEnd(getAfterLabelEnd(keySpec, labelEnd)) < 0) {
117            return;
118        }
119        throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
120    }
121
122    public static String getLabel(final String keySpec) {
123        if (keySpec == null) {
124            // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
125            return null;
126        }
127        if (hasIcon(keySpec)) {
128            return null;
129        }
130        final int labelEnd = indexOfLabelEnd(keySpec);
131        final String label = parseEscape(getBeforeLabelEnd(keySpec, labelEnd));
132        if (label.isEmpty()) {
133            throw new KeySpecParserError("Empty label: " + keySpec);
134        }
135        return label;
136    }
137
138    private static String getOutputTextInternal(final String keySpec, final int labelEnd) {
139        if (labelEnd <= 0) {
140            return null;
141        }
142        checkDoubleLabelEnd(keySpec, labelEnd);
143        return parseEscape(getAfterLabelEnd(keySpec, labelEnd));
144    }
145
146    public static String getOutputText(final String keySpec) {
147        if (keySpec == null) {
148            // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
149            return null;
150        }
151        final int labelEnd = indexOfLabelEnd(keySpec);
152        if (hasCode(keySpec, labelEnd)) {
153            return null;
154        }
155        final String outputText = getOutputTextInternal(keySpec, labelEnd);
156        if (outputText != null) {
157            if (StringUtils.codePointCount(outputText) == 1) {
158                // If output text is one code point, it should be treated as a code.
159                // See {@link #getCode(Resources, String)}.
160                return null;
161            }
162            if (outputText.isEmpty()) {
163                throw new KeySpecParserError("Empty outputText: " + keySpec);
164            }
165            return outputText;
166        }
167        final String label = getLabel(keySpec);
168        if (label == null) {
169            throw new KeySpecParserError("Empty label: " + keySpec);
170        }
171        // Code is automatically generated for one letter label. See {@link getCode()}.
172        return (StringUtils.codePointCount(label) == 1) ? null : label;
173    }
174
175    public static int getCode(final String keySpec, final KeyboardCodesSet codesSet) {
176        if (keySpec == null) {
177            // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
178            return CODE_UNSPECIFIED;
179        }
180        final int labelEnd = indexOfLabelEnd(keySpec);
181        if (hasCode(keySpec, labelEnd)) {
182            checkDoubleLabelEnd(keySpec, labelEnd);
183            return parseCode(getAfterLabelEnd(keySpec, labelEnd), codesSet, CODE_UNSPECIFIED);
184        }
185        final String outputText = getOutputTextInternal(keySpec, labelEnd);
186        if (outputText != null) {
187            // If output text is one code point, it should be treated as a code.
188            // See {@link #getOutputText(String)}.
189            if (StringUtils.codePointCount(outputText) == 1) {
190                return outputText.codePointAt(0);
191            }
192            return CODE_OUTPUT_TEXT;
193        }
194        final String label = getLabel(keySpec);
195        if (label == null) {
196            throw new KeySpecParserError("Empty label: " + keySpec);
197        }
198        // Code is automatically generated for one letter label.
199        return (StringUtils.codePointCount(label) == 1) ? label.codePointAt(0) : CODE_OUTPUT_TEXT;
200    }
201
202    // TODO: Make this method private once Key.code attribute is removed.
203    public static int parseCode(final String text, final KeyboardCodesSet codesSet,
204            final int defCode) {
205        if (text == null) {
206            return defCode;
207        }
208        if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) {
209            return codesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length()));
210        }
211        if (text.startsWith(PREFIX_HEX)) {
212            return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16);
213        }
214        return Integer.parseInt(text);
215    }
216
217    public static int getIconId(final String keySpec) {
218        if (keySpec == null) {
219            // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
220            return KeyboardIconsSet.ICON_UNDEFINED;
221        }
222        if (!hasIcon(keySpec)) {
223            return KeyboardIconsSet.ICON_UNDEFINED;
224        }
225        final int labelEnd = indexOfLabelEnd(keySpec);
226        final String iconName = getBeforeLabelEnd(keySpec, labelEnd)
227                .substring(KeyboardIconsSet.PREFIX_ICON.length());
228        return KeyboardIconsSet.getIconId(iconName);
229    }
230
231    @SuppressWarnings("serial")
232    public static final class KeySpecParserError extends RuntimeException {
233        public KeySpecParserError(final String message) {
234            super(message);
235        }
236    }
237}
238