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 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