/* * Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.HashMap; import java.util.HashSet; import java.util.ListResourceBundle; import java.util.Map; import java.util.Set; /** * Prepares new key names for Resources.java. * 6987827: security/util/Resources.java needs improvement * * Run inside jdk/src/share/classes: * * java NewResourcesNames $( * for a in $(find com/sun/security sun/security javax/security -type f); do * egrep -q '(ResourcesMgr.getString|rb.getString)' $a && echo $a * done) * * Before running this tool, run the following two commands to make sure there * are only these 2 types of calls into the resources: * for a in `find com/sun/security sun/security javax/security -type f`; do * cat $a | perl -ne 'print if /\bResourcesMgr\b/'; done | * grep -v ResourcesMgr.getString * for a in `find com/sun/security sun/security -type f`; do * cat $a | perl -ne 'print if /\brb\b/'; done | * grep -v rb.getString */ class NewResourcesNames { // Max length of normalized names static int MAXLEN = 127; static String[] resources = { "sun/security/tools/jarsigner/Resources.java", "sun/security/tools/keytool/Resources.java", "sun/security/tools/policytool/Resources.java", "sun/security/util/Resources.java", "sun/security/util/AuthResources.java", }; public static void main(String[] args) throws Exception { // Load all names inside resources files Map<String,String> allnames = loadResources(); // Modify the callers. There are two patterns: // 1. ResourcesMgr.getString(" // used by most JAAS codes // 2. rb.getString(" // used by tools Set<String> allfound = new HashSet<String>(); for (String arg: args) { allfound.addAll(rewriteFile(arg, "ResourcesMgr.getString(\"")); allfound.addAll(rewriteFile(arg, "rb.getString(\"")); } // Special case 1: KeyTool's enum definition of commands and options allfound.addAll(keyToolEnums()); // Special case 2: PolicyFile called this 4 times allfound.addAll(rewriteFile("sun/security/provider/PolicyFile.java", "ResourcesMgr.getString(POLICY+\"")); // During the calls above, you can read sth like: // // Working on com/sun/security/auth/PolicyParser.java // GOOD match is 17 // // This means a " exists right after getString(. Sometimes you see // // Working on sun/security/tools/keytool/Main.java // BAD!! pmatch != match: 212 != 209 // Working on sun/security/provider/PolicyFile.java // BAD!! pmatch != match: 14 != 10 // // which is mismatch. There are only two such special cases list above. // For KeyTool, there are 3 calls for showing help. For PolicyTool, 3 // for name prefixed with POLICY. They are covered in the two special // cases above. // Names used but not defined. This is mostly error, except for // special case 2 above. So it's OK to see 3 entries red here if (!allnames.keySet().containsAll(allfound)) { err("FATAL: Undefined names"); for (String name: allfound) { if (!allnames.keySet().contains(name)) { err(" " + name); } } } // Names defined but not used. Mostly this is old entries not removed. // When soemone remove a line of code, he dares not remove the entry // in case it's also used somewhere else. if (!allfound.containsAll(allnames.keySet())) { System.err.println("WARNING: Unused names"); for (String name: allnames.keySet()) { if (!allfound.contains(name)) { System.err.println(allnames.get(name)); System.err.println(" " + normalize(name)); System.err.println(" [" + name + "]"); } } } } /** * Loads the three resources files. Saves names into a Map. */ private static Map<String,String> loadResources() throws Exception { // Name vs Resource Map<String,String> allnames = new HashMap<String,String>(); for (String f: resources) { String clazz = f.replace('/', '.').substring(0, f.length()-5); Set<String> expected = loadClass(clazz); Set<String> found = rewriteFile(f, "{\""); // This is to check that word parsing is identical to Java thinks if (!expected.equals(found)) { throw new Exception("Expected and found do not match"); } for (String name: found) { allnames.put(name, f); } } return allnames; } /** * Special case treat for enums description in KeyTool */ private static Set<String> keyToolEnums() throws Exception { Set<String> names = new HashSet<String>(); String file = "sun/security/tools/keytool/Main.java"; System.err.println("Working on " + file); File origFile = new File(file); File tmpFile = new File(file + ".tmp"); origFile.renameTo(tmpFile); tmpFile.deleteOnExit(); BufferedReader br = new BufferedReader( new InputStreamReader(new FileInputStream(tmpFile))); PrintWriter out = new PrintWriter(new FileOutputStream(origFile)); int stage = 0; // 1. commands, 2. options, 3. finished int match = 0; while (true) { String s = br.readLine(); if (s == null) { break; } if (s.indexOf("enum Command") >= 0) stage = 1; else if (s.indexOf("enum Option") >= 0) stage = 2; else if (s.indexOf("private static final String JKS") >= 0) stage = 3; if (stage == 1 || stage == 2) { if (s.indexOf("(\"") >= 0) { match++; int p1, p2; if (stage == 1) { p1 = s.indexOf("\""); p2 = s.indexOf("\"", p1+1); } else { p2 = s.lastIndexOf("\""); p1 = s.lastIndexOf("\"", p2-1); } String name = s.substring(p1+1, p2); names.add(name); out.println(s.substring(0, p1+1) + normalize(name) + s.substring(p2)); } else { out.println(s); } } else { out.println(s); } } br.close(); out.close(); System.err.println(" GOOD match is " + match); return names; } /** * Loads a resources using JRE and returns the names */ private static Set<String> loadClass(String clazz) throws Exception { ListResourceBundle lrb = (ListResourceBundle)Class.forName(clazz).newInstance(); Set<String> keys = lrb.keySet(); Map<String,String> newold = new HashMap<String,String>(); boolean dup = false; // Check if normalize() creates dup entries. This is crucial. for (String k: keys) { String key = normalize(k); if (newold.containsKey(key)) { err("Dup found for " + key + ":"); err("["+newold.get(key)+"]"); err("["+k+"]"); dup = true; } newold.put(key, k); } if (dup) throw new Exception(); return keys; } /** * Rewrites a file using a pattern. The name string should be right after * the pattern. Note: pattern ignores whitespaces. Returns names found. */ private static Set<String> rewriteFile(String file, String pattern) throws Exception { System.err.println("Working on " + file); Set<String> names = new HashSet<String>(); int plen = pattern.length(); int match = 0; // The bare XXX.getString is also matched. Sometimes getString is // called but does not use literal strings. This is harder to solve. int pmatch = 0; int pheadlen = plen - 2; String phead = pattern.substring(0, plen-2); // The non-whitespace chars read since, used to check for pattern StringBuilder history = new StringBuilder(); int hlen = 0; File origFile = new File(file); File tmpFile = new File(file + ".tmp"); origFile.renameTo(tmpFile); tmpFile.deleteOnExit(); FileInputStream fis = new FileInputStream(tmpFile); FileOutputStream fos = new FileOutputStream(origFile); while (true) { int ch = fis.read(); if (ch < 0) break; if (!Character.isWhitespace(ch)) { history.append((char)ch); hlen++; if (pheadlen > 0 && hlen >= pheadlen && history.substring(hlen-pheadlen, hlen).equals(phead)) { pmatch++; } } if (hlen >= plen && history.substring(hlen-plen, hlen).equals(pattern)) { match++; history = new StringBuilder(); hlen = 0; fos.write(ch); // Save a name StringBuilder sb = new StringBuilder(); // Save things after the second ". Maybe it's an end, maybe // it's just literal string concatenation. StringBuilder tail = new StringBuilder(); boolean in = true; // inside name string while (true) { int n = fis.read(); if (in) { if (n == '\\') { int second = fis.read(); switch (second) { case 'n': sb.append('\n'); break; case 'r': sb.append('\r'); break; case 't': sb.append('\t'); break; case '"': sb.append('"'); break; default: throw new Exception(String.format( "I don't know this escape: %s%c", sb.toString(), second)); } } else if (n == '"') { in = false; // Maybe string concat? say bytes until clear tail = new StringBuilder(); tail.append('"'); } else { sb.append((char)n); } } else { tail.append((char)n); if (n == '"') { // string concat, in again in = true; } else if (n == ',' || n == ')') { // real end break; } else if (Character.isWhitespace(n) || n == '+') { // string concat } else { throw new Exception("Not a correct concat"); } } } String s = sb.toString(); names.add(s); fos.write(normalize(s).getBytes()); fos.write(tail.toString().getBytes()); } else { fos.write(ch); } } // Check pheadlen > 0. Don't want to mess with rewrite for resources if (pheadlen > 0 && pmatch != match) { err(" BAD!! pmatch != match: " + pmatch + " != " + match); } else { System.err.println(" GOOD match is " + match); } fis.close(); fos.close(); return names; } /** * Normalize a string. Rules: * * 1. If all spacebar return "nSPACE", n is count * 2. If consisting at least one alphanumeric: * a. All alphanumeric remain * b. All others in a row goes to a single ".", even if at head or tail * 3. Otherwise: * a. "****\n\n" to "STARNN", special case * b. the English name if first char in *,.\n():'" * * Current observations show there's no dup, Hurray! Otherwise, add more * special cases. */ private static String normalize(String s) throws Exception { boolean needDot = false; // All spacebar case int n = 0; for (char c: s.toCharArray()) { if (c == ' ') n++; else n = -10000; } if (n == 1) return "SPACE"; else if (n > 1) return "" + n + "SPACE"; StringBuilder sb = new StringBuilder(); int dotpos = -1; for (int i=0; i<s.length(); i++) { char c = s.charAt(i); if (Character.isLetter(c) || Character.isDigit(c) || c == '{' || c == '}') { if (needDot) { // Rememeber the last dot, we want shorter form nice if (sb.length() <= MAXLEN) dotpos = sb.length(); // "." only added when an alphanumeric is seen. This makes // sure sb is empty when there's no alphanumerics at all sb.append("."); } sb.append(c); needDot = false; } else { needDot = true; } } // No alphanemeric? if (sb.length() == 0) { if (s.contains("*") && s.contains("\n")) { return "STARNN"; } for (char c: s.toCharArray()) { switch (c) { case '*': return "STAR"; case ',': return "COMMA"; case '.': return "PERIOD"; case '\n': return "NEWLINE"; case '(': return "LPARAM"; case ')': return "RPARAM"; case ':': return "COLON"; case '\'': case '"': return "QUOTE"; } } throw new Exception("Unnamed char: [" + s + "]"); } // tail "." only added when there are alphanumerics if (needDot) sb.append('.'); String res = sb.toString(); if (res.length() > MAXLEN) { if (dotpos < 0) throw new Exception("No dot all over? " + s); return res.substring(0, dotpos); } else { return res; } } private static void err(String string) { System.out.println("\u001b[1;37;41m" + string + "\u001b[m"); } }