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 
17 package com.android.inputmethod.keyboard.tools;
18 
19 import java.io.File;
20 import java.io.IOException;
21 import java.io.InputStreamReader;
22 import java.io.LineNumberReader;
23 import java.io.PrintStream;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.Comparator;
27 import java.util.HashMap;
28 import java.util.Locale;
29 import java.util.TreeMap;
30 import java.util.jar.JarFile;
31 
32 public 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 
MoreKeysResources(final JarFile jar)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 
writeToJava(final String outDir)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 
inflateTemplate(final LineNumberReader in, final PrintStream out)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 
dumpNames(final PrintStream out)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 
dumpDefaultTexts(final PrintStream out)164     private void dumpDefaultTexts(final PrintStream out) {
165         final int outputArraySize = dumpTextsInternal(out, mDefaultResourceMap);
166         mDefaultResourceMap.setOutputArraySize(outputArraySize);
167     }
168 
getArrayNameForLocale(final Locale locale)169     private static String getArrayNameForLocale(final Locale locale) {
170         return TEXTS_ARRAY_NAME_PREFIX + LocaleUtils.getLocaleCode(locale);
171     }
172 
dumpTexts(final PrintStream out)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 
dumpLocalesMap(final PrintStream out)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 
dumpTextsInternal(final PrintStream out, final StringResourceMap resMap)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 
addPrefix(final String prefix, final String lines)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 
escapeNonAscii(final String text)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