1 package org.unicode.icu.tool.cldrtoicu; 2 3 import static com.google.common.base.Preconditions.checkArgument; 4 import static com.google.common.base.Preconditions.checkState; 5 import static java.nio.charset.StandardCharsets.UTF_8; 6 7 import java.io.ByteArrayInputStream; 8 import java.io.IOException; 9 import java.io.InputStream; 10 import java.io.InputStreamReader; 11 import java.nio.file.Files; 12 import java.nio.file.Path; 13 import java.nio.file.Paths; 14 import java.util.Collection; 15 import java.util.function.Function; 16 import java.util.logging.Formatter; 17 import java.util.logging.LogManager; 18 import java.util.logging.LogRecord; 19 20 import org.unicode.cldr.api.CldrDataSupplier; 21 22 import com.google.common.base.CharMatcher; 23 import com.google.common.base.Joiner; 24 import com.google.common.collect.ImmutableList; 25 import com.google.common.io.CharStreams; 26 27 /** 28 * Utility to allow any "mapper" class to trivially support a main method and useful 29 * logging behaviour to help avoid the need for ad-hoc logging via {@code System.out}. 30 * 31 * <p>In most cases a mapping class can just have a {@code main} method like: 32 * <pre>{@code 33 * // Arguments: <output-dir> [<log-level>] 34 * public static void main(String[] args) throws IOException { 35 * DebugWriter.writeForDebugging(args, MapperClass::process); 36 * } 37 * }</pre> 38 * 39 * <p>Note however that running this still requires that the {@code CLDR_DIR} system 40 * property is set. 41 */ 42 public final class DebugWriter { 43 private static final String PACKAGE_ROOT = "org.unicode.icu.tool.cldrtoicu"; 44 private static final String PACKAGE_PREFIX = PACKAGE_ROOT + "."; 45 46 /** 47 * Writes the IcuData generated by the given function using the default {@code CLDR_DIR} 48 * system property. 49 * 50 * <p>This is a helper method to make it easy for each mapper to have its own main 51 * method for debugging, and it should not be used directly by {@code LdmlConverter}. 52 */ writeForDebugging(String[] args, Function<CldrDataSupplier, IcuData> fn)53 public static void writeForDebugging(String[] args, Function<CldrDataSupplier, IcuData> fn) 54 throws IOException { 55 writeMultipleForDebugging(args, src -> ImmutableList.of(fn.apply(src))); 56 } 57 58 /** 59 * Writes the IcuData generated by the given function using the default {@code CLDR_DIR} 60 * system property. 61 * 62 * <p>This is a helper method to make it easy for each mapper to have its own main 63 * method for debugging, and it should not be used directly by {@code LdmlConverter}. 64 */ writeMultipleForDebugging( String[] args, Function<CldrDataSupplier, Collection<IcuData>> fn)65 public static void writeMultipleForDebugging( 66 String[] args, Function<CldrDataSupplier, Collection<IcuData>> fn) 67 throws IOException { 68 String cldrPath = System.getProperty("CLDR_DIR", System.getenv("CLDR_DIR")); 69 checkState(cldrPath != null, 70 "required 'CLDR_DIR' system property or environment variable not set"); 71 checkArgument(args.length >= 1, "expected output directory"); 72 Path outDir = Paths.get(args[0]); 73 String logLevel = (args.length == 2) ? args[1] : "OFF"; 74 75 String loggerConfig = Joiner.on("\n").join( 76 "handlers = java.util.logging.ConsoleHandler", 77 "java.util.logging.ConsoleHandler.level = ALL", 78 "java.util.logging.ConsoleHandler.encoding = UTF-8", 79 "java.util.logging.ConsoleHandler.formatter = " + LogFormatter.class.getName(), 80 "", 81 PACKAGE_ROOT + ".level = " + logLevel); 82 LogManager.getLogManager() 83 .readConfiguration(new ByteArrayInputStream(loggerConfig.getBytes(UTF_8))); 84 85 Files.createDirectories(outDir); 86 CldrDataSupplier src = CldrDataSupplier.forCldrFilesIn(Paths.get(cldrPath)); 87 ImmutableList<String> header = readLinesFromResource("/ldml2icu_header.txt"); 88 for (IcuData icuData : fn.apply(src)) { 89 IcuTextWriter.writeToFile(icuData, outDir, header, true); 90 } 91 } 92 readLinesFromResource(String name)93 private static ImmutableList<String> readLinesFromResource(String name) { 94 try (InputStream in = DebugWriter.class.getResourceAsStream(name)) { 95 return ImmutableList.copyOf(CharStreams.readLines(new InputStreamReader(in, UTF_8))); 96 } catch (IOException e) { 97 throw new RuntimeException("cannot read resource: " + name, e); 98 } 99 } 100 101 // Format is "<localClass>#<plainMethod>: <message>" since this is a fairly 102 // small code base and keeping logs concise is helpful. 103 // This is only public because it has to be reflectively instantiated. 104 public static final class LogFormatter extends Formatter { 105 private static final CharMatcher SEPARATORS = CharMatcher.anyOf("$#"); 106 107 @Override format(LogRecord logRecord)108 public String format(LogRecord logRecord) { 109 String message = String.format("%s#%s: %s\n", 110 localClassName(logRecord.getSourceClassName()), 111 plainMethodName(logRecord.getSourceMethodName()), 112 logRecord.getMessage()); 113 if (logRecord.getThrown() != null) { 114 message += logRecord.getThrown() + "\n"; 115 } 116 return message; 117 } 118 119 // Since everything is in the same base package, elide that (if present). localClassName(String className)120 private String localClassName(String className) { 121 return className.startsWith(PACKAGE_PREFIX) 122 ? className.substring(className.lastIndexOf(".") + 1) 123 : className; 124 } 125 126 // Trim method names to remove things like lambda prefixes and anonymous 127 // class suffixes (these add noise to every log and aren't that useful). plainMethodName(String methodName)128 private String plainMethodName(String methodName) { 129 if (methodName.startsWith("lambda$")) { 130 methodName = methodName.substring("lambda$".length()); 131 } 132 if (SEPARATORS.matchesAnyOf(methodName)) { 133 methodName = methodName.substring(0, SEPARATORS.indexIn(methodName)); 134 } 135 return methodName; 136 } 137 } 138 } 139