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 android.content.res.Resources;
20import android.text.TextUtils;
21import android.util.Log;
22
23import com.android.inputmethod.keyboard.Keyboard;
24import com.android.inputmethod.latin.R;
25
26import java.util.ArrayList;
27
28/**
29 * String parser of moreKeys attribute of Key.
30 * The string is comma separated texts each of which represents one "more key".
31 * Each "more key" specification is one of the following:
32 * - A single letter (Letter)
33 * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText).
34 * - Icon followed by keyOutputText or code (@icon/icon_number|@integer/key_code)
35 * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\'
36 * character.
37 * Note that the character '@' and '\' are also parsed by XML parser and CSV parser as well.
38 * See {@link KeyboardIconsSet} about icon_number.
39 */
40public class MoreKeySpecParser {
41    private static final String TAG = MoreKeySpecParser.class.getSimpleName();
42
43    private static final char ESCAPE = '\\';
44    private static final String LABEL_END = "|";
45    private static final String PREFIX_AT = "@";
46    private static final String PREFIX_ICON = PREFIX_AT + "icon/";
47    private static final String PREFIX_CODE = PREFIX_AT + "integer/";
48
49    private MoreKeySpecParser() {
50        // Intentional empty constructor for utility class.
51    }
52
53    private static boolean hasIcon(String moreKeySpec) {
54        if (moreKeySpec.startsWith(PREFIX_ICON)) {
55            final int end = indexOfLabelEnd(moreKeySpec, 0);
56            if (end > 0)
57                return true;
58            throw new MoreKeySpecParserError("outputText or code not specified: " + moreKeySpec);
59        }
60        return false;
61    }
62
63    private static boolean hasCode(String moreKeySpec) {
64        final int end = indexOfLabelEnd(moreKeySpec, 0);
65        if (end > 0 && end + 1 < moreKeySpec.length()
66                && moreKeySpec.substring(end + 1).startsWith(PREFIX_CODE)) {
67            return true;
68        }
69        return false;
70    }
71
72    private static String parseEscape(String text) {
73        if (text.indexOf(ESCAPE) < 0)
74            return text;
75        final int length = text.length();
76        final StringBuilder sb = new StringBuilder();
77        for (int pos = 0; pos < length; pos++) {
78            final char c = text.charAt(pos);
79            if (c == ESCAPE && pos + 1 < length) {
80                sb.append(text.charAt(++pos));
81            } else {
82                sb.append(c);
83            }
84        }
85        return sb.toString();
86    }
87
88    private static int indexOfLabelEnd(String moreKeySpec, int start) {
89        if (moreKeySpec.indexOf(ESCAPE, start) < 0) {
90            final int end = moreKeySpec.indexOf(LABEL_END, start);
91            if (end == 0)
92                throw new MoreKeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec);
93            return end;
94        }
95        final int length = moreKeySpec.length();
96        for (int pos = start; pos < length; pos++) {
97            final char c = moreKeySpec.charAt(pos);
98            if (c == ESCAPE && pos + 1 < length) {
99                pos++;
100            } else if (moreKeySpec.startsWith(LABEL_END, pos)) {
101                return pos;
102            }
103        }
104        return -1;
105    }
106
107    public static String getLabel(String moreKeySpec) {
108        if (hasIcon(moreKeySpec))
109            return null;
110        final int end = indexOfLabelEnd(moreKeySpec, 0);
111        final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end))
112                : parseEscape(moreKeySpec);
113        if (TextUtils.isEmpty(label))
114            throw new MoreKeySpecParserError("Empty label: " + moreKeySpec);
115        return label;
116    }
117
118    public static String getOutputText(String moreKeySpec) {
119        if (hasCode(moreKeySpec))
120            return null;
121        final int end = indexOfLabelEnd(moreKeySpec, 0);
122        if (end > 0) {
123            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0)
124                    throw new MoreKeySpecParserError("Multiple " + LABEL_END + ": "
125                            + moreKeySpec);
126            final String outputText = parseEscape(moreKeySpec.substring(end + LABEL_END.length()));
127            if (!TextUtils.isEmpty(outputText))
128                return outputText;
129            throw new MoreKeySpecParserError("Empty outputText: " + moreKeySpec);
130        }
131        final String label = getLabel(moreKeySpec);
132        if (label == null)
133            throw new MoreKeySpecParserError("Empty label: " + moreKeySpec);
134        // Code is automatically generated for one letter label. See {@link getCode()}.
135        if (label.length() == 1)
136            return null;
137        return label;
138    }
139
140    public static int getCode(Resources res, String moreKeySpec) {
141        if (hasCode(moreKeySpec)) {
142            final int end = indexOfLabelEnd(moreKeySpec, 0);
143            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0)
144                throw new MoreKeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec);
145            final int resId = getResourceId(res,
146                    moreKeySpec.substring(end + LABEL_END.length() + PREFIX_AT.length()));
147            final int code = res.getInteger(resId);
148            return code;
149        }
150        if (indexOfLabelEnd(moreKeySpec, 0) > 0)
151            return Keyboard.CODE_DUMMY;
152        final String label = getLabel(moreKeySpec);
153        // Code is automatically generated for one letter label.
154        if (label != null && label.length() == 1)
155            return label.charAt(0);
156        return Keyboard.CODE_DUMMY;
157    }
158
159    public static int getIconId(String moreKeySpec) {
160        if (hasIcon(moreKeySpec)) {
161            int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length() + 1);
162            final String iconId = moreKeySpec.substring(PREFIX_ICON.length(), end);
163            try {
164                return Integer.valueOf(iconId);
165            } catch (NumberFormatException e) {
166                Log.w(TAG, "illegal icon id specified: " + iconId);
167                return KeyboardIconsSet.ICON_UNDEFINED;
168            }
169        }
170        return KeyboardIconsSet.ICON_UNDEFINED;
171    }
172
173    private static int getResourceId(Resources res, String name) {
174        String packageName = res.getResourcePackageName(R.string.english_ime_name);
175        int resId = res.getIdentifier(name, null, packageName);
176        if (resId == 0)
177            throw new MoreKeySpecParserError("Unknown resource: " + name);
178        return resId;
179    }
180
181    @SuppressWarnings("serial")
182    public static class MoreKeySpecParserError extends RuntimeException {
183        public MoreKeySpecParserError(String message) {
184            super(message);
185        }
186    }
187
188    public interface CodeFilter {
189        public boolean shouldFilterOut(int code);
190    }
191
192    public static final CodeFilter DIGIT_FILTER = new CodeFilter() {
193        @Override
194        public boolean shouldFilterOut(int code) {
195            return Character.isDigit(code);
196        }
197    };
198
199    public static CharSequence[] filterOut(Resources res, CharSequence[] moreKeys,
200            CodeFilter filter) {
201        if (moreKeys == null || moreKeys.length < 1) {
202            return null;
203        }
204        if (moreKeys.length == 1
205                && filter.shouldFilterOut(getCode(res, moreKeys[0].toString()))) {
206            return null;
207        }
208        ArrayList<CharSequence> filtered = null;
209        for (int i = 0; i < moreKeys.length; i++) {
210            final CharSequence moreKeySpec = moreKeys[i];
211            if (filter.shouldFilterOut(getCode(res, moreKeySpec.toString()))) {
212                if (filtered == null) {
213                    filtered = new ArrayList<CharSequence>();
214                    for (int j = 0; j < i; j++) {
215                        filtered.add(moreKeys[j]);
216                    }
217                }
218            } else if (filtered != null) {
219                filtered.add(moreKeySpec);
220            }
221        }
222        if (filtered == null) {
223            return moreKeys;
224        }
225        if (filtered.size() == 0) {
226            return null;
227        }
228        return filtered.toArray(new CharSequence[filtered.size()]);
229    }
230}
231