1/*
2 * Copyright (C) 2012 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.tools;
18
19import java.io.File;
20import java.io.IOException;
21import java.io.InputStreamReader;
22import java.io.LineNumberReader;
23import java.io.PrintStream;
24import java.util.ArrayList;
25import java.util.Collections;
26import java.util.Comparator;
27import java.util.HashMap;
28import java.util.Locale;
29import java.util.TreeMap;
30import java.util.jar.JarFile;
31
32public class MoreKeysResources {
33    private static final String TEXT_RESOURCE_NAME = "donottranslate-more-keys.xml";
34
35    private static final String JAVA_TEMPLATE = "KeyboardTextsTable.tmpl";
36    private static final String MARK_NAMES = "@NAMES@";
37    private static final String MARK_DEFAULT_TEXTS = "@DEFAULT_TEXTS@";
38    private static final String MARK_TEXTS = "@TEXTS@";
39    private static final String TEXTS_ARRAY_NAME_PREFIX = "TEXTS_";
40    private static final String MARK_LOCALES_AND_TEXTS = "@LOCALES_AND_TEXTS@";
41    private static final String EMPTY_STRING_VAR = "EMPTY";
42
43    private final JarFile mJar;
44    // String resources maps sorted by its language. The language is determined from the jar entry
45    // name by calling {@link JarUtils#getLocaleFromEntryName(String)}.
46    private final TreeMap<String, StringResourceMap> mResourcesMap = new TreeMap<>();
47    // Default string resources map.
48    private final StringResourceMap mDefaultResourceMap;
49    // Histogram of string resource names. This is used to sort {@link #mSortedResourceNames}.
50    private final HashMap<String, Integer> mNameHistogram = new HashMap<>();
51    // Sorted string resource names array; Descending order of histogram count.
52    // The string resource name is specified as an attribute "name" in string resource files.
53    // The string resource can be accessed by specifying name "!text/<name>"
54    // via {@link KeyboardTextsSet#getText(String)}.
55    private final String[] mSortedResourceNames;
56
57    public MoreKeysResources(final JarFile jar) {
58        mJar = jar;
59        final ArrayList<String> resourceEntryNames = JarUtils.getEntryNameListing(
60                jar, TEXT_RESOURCE_NAME);
61        for (final String entryName : resourceEntryNames) {
62            final StringResourceMap resMap = new StringResourceMap(entryName);
63            mResourcesMap.put(LocaleUtils.getLocaleCode(resMap.mLocale), resMap);
64        }
65        mDefaultResourceMap = mResourcesMap.get(
66                LocaleUtils.getLocaleCode(LocaleUtils.DEFAULT_LOCALE));
67
68        // Initialize name histogram and names list.
69        final HashMap<String, Integer> nameHistogram = mNameHistogram;
70        final ArrayList<String> resourceNamesList = new ArrayList<>();
71        for (final StringResource res : mDefaultResourceMap.getResources()) {
72            nameHistogram.put(res.mName, 0); // Initialize histogram value.
73            resourceNamesList.add(res.mName);
74        }
75        // Make name histogram.
76        for (final String locale : mResourcesMap.keySet()) {
77            final StringResourceMap resMap = mResourcesMap.get(locale);
78            if (resMap == mDefaultResourceMap) continue;
79            for (final StringResource res : resMap.getResources()) {
80                if (!mDefaultResourceMap.contains(res.mName)) {
81                    throw new RuntimeException(res.mName + " in " + locale
82                            + " doesn't have default resource");
83                }
84                final int histogramValue = nameHistogram.get(res.mName);
85                nameHistogram.put(res.mName, histogramValue + 1);
86            }
87        }
88        // Sort names list.
89        Collections.sort(resourceNamesList, new Comparator<String>() {
90            @Override
91            public int compare(final String leftName, final String rightName) {
92                final int leftCount = nameHistogram.get(leftName);
93                final int rightCount = nameHistogram.get(rightName);
94                // Descending order of histogram count.
95                if (leftCount > rightCount) return -1;
96                if (leftCount < rightCount) return 1;
97                // TODO: Add further criteria to order the same histogram value names to be able to
98                // minimize footprints of string resources arrays.
99                return 0;
100            }
101        });
102        mSortedResourceNames = resourceNamesList.toArray(new String[resourceNamesList.size()]);
103    }
104
105    public void writeToJava(final String outDir) {
106        final ArrayList<String> list = JarUtils.getEntryNameListing(mJar, JAVA_TEMPLATE);
107        if (list.isEmpty()) {
108            throw new RuntimeException("Can't find java template " + JAVA_TEMPLATE);
109        }
110        if (list.size() > 1) {
111            throw new RuntimeException("Found multiple java template " + JAVA_TEMPLATE);
112        }
113        final String template = list.get(0);
114        final String javaPackage = template.substring(0, template.lastIndexOf('/'));
115        PrintStream ps = null;
116        LineNumberReader lnr = null;
117        try {
118            if (outDir == null) {
119                ps = System.out;
120            } else {
121                final File outPackage = new File(outDir, javaPackage);
122                final File outputFile = new File(outPackage,
123                        JAVA_TEMPLATE.replace(".tmpl", ".java"));
124                outPackage.mkdirs();
125                ps = new PrintStream(outputFile, "UTF-8");
126            }
127            lnr = new LineNumberReader(new InputStreamReader(JarUtils.openResource(template)));
128            inflateTemplate(lnr, ps);
129        } catch (IOException e) {
130            throw new RuntimeException(e);
131        } finally {
132            JarUtils.close(lnr);
133            JarUtils.close(ps);
134        }
135    }
136
137    private void inflateTemplate(final LineNumberReader in, final PrintStream out)
138            throws IOException {
139        String line;
140        while ((line = in.readLine()) != null) {
141            if (line.contains(MARK_NAMES)) {
142                dumpNames(out);
143            } else if (line.contains(MARK_DEFAULT_TEXTS)) {
144                dumpDefaultTexts(out);
145            } else if (line.contains(MARK_TEXTS)) {
146                dumpTexts(out);
147            } else if (line.contains(MARK_LOCALES_AND_TEXTS)) {
148                dumpLocalesMap(out);
149            } else {
150                out.println(line);
151            }
152        }
153    }
154
155    private void dumpNames(final PrintStream out) {
156        final int namesCount = mSortedResourceNames.length;
157        for (int index = 0; index < namesCount; index++) {
158            final String name = mSortedResourceNames[index];
159            final int histogramValue = mNameHistogram.get(name);
160            out.format("        /* %3d:%2d */ \"%s\",\n", index, histogramValue, name);
161        }
162    }
163
164    private void dumpDefaultTexts(final PrintStream out) {
165        final int outputArraySize = dumpTextsInternal(out, mDefaultResourceMap);
166        mDefaultResourceMap.setOutputArraySize(outputArraySize);
167    }
168
169    private static String getArrayNameForLocale(final Locale locale) {
170        return TEXTS_ARRAY_NAME_PREFIX + LocaleUtils.getLocaleCode(locale);
171    }
172
173    private void dumpTexts(final PrintStream out) {
174        for (final StringResourceMap resMap : mResourcesMap.values()) {
175            final Locale locale = resMap.mLocale;
176            if (resMap == mDefaultResourceMap) continue;
177            out.format("    /* Locale %s: %s */\n",
178                    locale, LocaleUtils.getLocaleDisplayName(locale));
179            out.format("    private static final String[] " + getArrayNameForLocale(locale)
180                    + " = {\n");
181            final int outputArraySize = dumpTextsInternal(out, resMap);
182            resMap.setOutputArraySize(outputArraySize);
183            out.format("    };\n\n");
184        }
185    }
186
187    private void dumpLocalesMap(final PrintStream out) {
188        for (final StringResourceMap resMap : mResourcesMap.values()) {
189            final Locale locale = resMap.mLocale;
190            final String localeStr = LocaleUtils.getLocaleCode(locale);
191            final String localeToDump = (locale == LocaleUtils.DEFAULT_LOCALE)
192                    ? String.format("\"%s\"", localeStr)
193                    : String.format("\"%s\"%s", localeStr, "       ".substring(localeStr.length()));
194            out.format("        %s, %-12s /* %3d/%3d %s */\n",
195                    localeToDump, getArrayNameForLocale(locale) + ",",
196                    resMap.getResources().size(), resMap.getOutputArraySize(),
197                    LocaleUtils.getLocaleDisplayName(locale));
198        }
199    }
200
201    private int dumpTextsInternal(final PrintStream out, final StringResourceMap resMap) {
202        final ArrayInitializerFormatter formatter =
203                new ArrayInitializerFormatter(out, 100, "        ", mSortedResourceNames);
204        int outputArraySize = 0;
205        boolean successiveNull = false;
206        final int namesCount = mSortedResourceNames.length;
207        for (int index = 0; index < namesCount; index++) {
208            final String name = mSortedResourceNames[index];
209            final StringResource res = resMap.get(name);
210            if (res != null) {
211                // TODO: Check whether the resource value is equal to the default.
212                if (res.mComment != null) {
213                    formatter.outCommentLines(addPrefix("        // ", res. mComment));
214                }
215                final String escaped = escapeNonAscii(res.mValue);
216                if (escaped.length() == 0) {
217                    formatter.outElement(EMPTY_STRING_VAR + ",");
218                } else {
219                    formatter.outElement(String.format("\"%s\",", escaped));
220                }
221                successiveNull = false;
222                outputArraySize = formatter.getCurrentIndex();
223            } else {
224                formatter.outElement("null,");
225                successiveNull = true;
226            }
227        }
228        if (!successiveNull) {
229            formatter.flush();
230        }
231        return outputArraySize;
232    }
233
234    private static String addPrefix(final String prefix, final String lines) {
235        final StringBuilder sb = new StringBuilder();
236        for (final String line : lines.split("\n")) {
237            sb.append(prefix + line.trim() + "\n");
238        }
239        return sb.toString();
240    }
241
242    private static String escapeNonAscii(final String text) {
243        final StringBuilder sb = new StringBuilder();
244        final int length = text.length();
245        for (int i = 0; i < length; i++) {
246            final char c = text.charAt(i);
247            if (c >= ' ' && c < 0x7f) {
248                sb.append(c);
249            } else {
250                sb.append(String.format("\\u%04X", (int)c));
251            }
252        }
253        return sb.toString();
254    }
255}
256