1 import com.github.javaparser.utils.Log; 2 3 import java.io.BufferedReader; 4 import java.io.Closeable; 5 import java.io.File; 6 import java.io.FileOutputStream; 7 import java.io.FileReader; 8 import java.io.IOException; 9 import java.io.PrintStream; 10 import java.util.ArrayList; 11 import java.util.HashMap; 12 import java.util.List; 13 import java.util.Set; 14 import java.util.StringJoiner; 15 import java.util.logging.Logger; 16 import java.util.regex.Matcher; 17 import java.util.regex.Pattern; 18 19 public class StatsGenerator { 20 private static final Logger LOGGER = Logger.getLogger(StatsGenerator.class.toString()); 21 private static final boolean DEBUG = false; 22 private static final Pattern SINGLE_ENTRY = Pattern.compile( 23 "^\\s*((optional|repeated) )?(([a-zA-Z_0-9\\.]+)\\s+)?([a-zA-Z_0-9]+)\\s*=\\s*(\\-?\\d+)([^\\d;][^\\;]*)?;$"); 24 25 private final List<String> mAllImports = new ArrayList<>(); 26 private final File mRootPath; 27 StatsGenerator(File rootPath)28 public StatsGenerator(File rootPath) { 29 mRootPath = rootPath; 30 } 31 process(File atomFile, Set<File> atomExtensions, String module, String packageName, File outputFile)32 public void process(File atomFile, Set<File> atomExtensions, String module, String packageName, 33 File outputFile) throws IOException { 34 PrintStream output = new PrintStream(new FileOutputStream(outputFile)); 35 36 output.println("package " + packageName + ";"); 37 output.println(); 38 output.println("import android.util.StatsEvent;"); 39 output.println(); 40 41 String className = outputFile.getName(); 42 className = className.substring(0, className.indexOf(".")); 43 output.println("public class " + className + " { "); 44 output.println(); 45 46 GroupEntry out = new GroupEntry(null); 47 parseFile(out, atomFile); 48 49 if (atomExtensions != null) { 50 for (File ext : atomExtensions) { 51 parseExtension(out, ext); 52 } 53 } 54 55 GroupEntry atom = out.findGroup("atom"); 56 GroupEntry pulledGroup = atom.findGroup("pulled"); 57 List<SingleEntry> children = new ArrayList<>(); 58 children.addAll(atom.findGroup("pushed").getSingles()); 59 children.addAll(atom.findGroup("pulled").getSingles()); 60 61 for (SingleEntry e : atom.getSingles()) { 62 if (e.extra.contains(module)) { 63 e.writeTo("", output); 64 output.println(); 65 66 System.out.println(">> " + out.findGroup(e.type) + " " + e.type); 67 printGroup(out.findGroup(e.type), output, convertToSymbolGroupPrefix(e.type)); 68 output.println(); 69 output.println(); 70 } 71 } 72 73 for (SingleEntry e : children) { 74 if (e.extra.contains(module)) { 75 e.writeTo("", output); 76 output.println(); 77 78 if (DEBUG) System.out.println(">> " + out.findGroup(e.type) + " " + e.type); 79 printGroup(out.findGroup(e.type), output, convertToSymbolGroupPrefix(e.type)); 80 output.println(); 81 output.println(); 82 } 83 } 84 85 for (SingleEntry e : pulledGroup.getSingles()) { 86 if (e.extra.contains(module)) { 87 GroupEntry group = out.findGroup(e.type); 88 output.println(group.constructBuildStatsEventMethod()); 89 } 90 } 91 92 // Add a Placeholder write method 93 output.println(" // Placeholder code for local development only"); 94 output.println(" public static void write(int code, Object... params) { }"); 95 output.println(); 96 output.println("}"); 97 output.close(); 98 } 99 printGroup(GroupEntry entry, PrintStream output, String prefix)100 private static void printGroup(GroupEntry entry, PrintStream output, String prefix) { 101 for (SingleEntry e : entry.getSingles()) { 102 GroupEntry subGroup = entry.findGroup(e.type); 103 if (subGroup != null) { 104 printGroup(subGroup, output, prefix + convertToSymbolGroupPrefix(e.name)); 105 } else { 106 switch (e.type) { 107 case "bool": 108 case "int32": 109 case "int64": 110 case "float": 111 case "string": 112 case "null": // In case of enum 113 e.writeTo(prefix, output); 114 break; 115 default: 116 LOGGER.warning("Type not found " + e); 117 } 118 } 119 } 120 } 121 convertToSymbolGroupPrefix(String name)122 private static String convertToSymbolGroupPrefix(String name) { 123 int dot = name.lastIndexOf('.'); 124 if (dot >= 0) { 125 name = name.substring(dot + 1); 126 } 127 return name.replaceAll("([a-z])([A-Z])", "$1_$2").toUpperCase() + "__"; 128 } 129 parseFile(GroupEntry out, File path)130 private String parseFile(GroupEntry out, File path) throws IOException { 131 ArrayList<String> outImports = new ArrayList<>(); 132 String outerPath; 133 try (MyReader reader = new MyReader(new BufferedReader(new FileReader(path)), outImports)) { 134 parseGroup(out, reader, ""); 135 out.javaPackage = reader.javaPackage; 136 outerPath = reader.rootPrefix; 137 } 138 parseImports(outImports, out, false); 139 return outerPath; 140 } 141 parseImports(ArrayList<String> imports, GroupEntry out, boolean skipDuplicate)142 private void parseImports(ArrayList<String> imports, GroupEntry out, 143 boolean skipDuplicate) throws IOException { 144 for (String p : imports) { 145 if (mAllImports.contains(p) && skipDuplicate) { 146 System.err.println("Importing already parsed file " + p); 147 continue; 148 } 149 mAllImports.add(p); 150 File importFile = new File(mRootPath, p); 151 if (importFile.exists()) { 152 GroupEntry grp = new GroupEntry(null); 153 String pkg = parseFile(grp, importFile); 154 155 GroupEntry grp2 = out.imports.get(pkg); 156 if (grp2 == null) { 157 out.imports.put(pkg, grp); 158 } else { 159 grp2.children.addAll(grp.children); 160 grp2.imports.putAll(grp.imports); 161 } 162 } 163 } 164 } 165 parseExtension(GroupEntry out, File path)166 private void parseExtension(GroupEntry out, File path) throws IOException { 167 ArrayList<String> outImports = new ArrayList<>(); 168 try (MyReader reader = new MyReader(new BufferedReader(new FileReader(path)), outImports)) { 169 String line = null; 170 try { 171 while (!(line = reader.getEntry()).startsWith("}")) { 172 if (line.endsWith("{")) { 173 String prefix = ""; 174 if (DEBUG) System.out.println(prefix + " :: " + line); 175 String[] parts = line.split(" ", 3); 176 177 GroupEntry group = new GroupEntry(out.root); 178 group.name = parts[1]; 179 group.type = parts[0]; 180 181 GroupEntry existing = out.findGroup(group.name); 182 if (existing != null) { 183 if (!"extend".equals(group.type)) { 184 System.out.println("Found duplicated entry without extension"); 185 continue; 186 } 187 parseGroup(existing, reader, prefix + " "); 188 } else { 189 parseGroup(group, reader, prefix + " "); 190 out.children.add(group); 191 } 192 } 193 } 194 } catch (RuntimeException e) { 195 LOGGER.warning("Error at line " + line); 196 throw e; 197 } 198 199 parseGroup(out, reader, ""); 200 } 201 202 parseImports(outImports, out, true); 203 } 204 parseGroup(GroupEntry out, MyReader reader, String prefix)205 private static void parseGroup(GroupEntry out, MyReader reader, String prefix) 206 throws IOException { 207 String line = null; 208 try { 209 while (!(line = reader.getEntry()).startsWith("}")) { 210 Entry entry; 211 if (line.endsWith("{")) { 212 if (DEBUG) System.out.println(prefix + " :: " + line); 213 String[] parts = line.split(" ", 3); 214 215 GroupEntry group = new GroupEntry(out.root); 216 group.name = parts[1]; 217 group.type = parts[0]; 218 219 parseGroup(group, reader, prefix + " "); 220 entry = group; 221 } else { 222 String ot = line; 223 Matcher m = SINGLE_ENTRY.matcher(line.trim()); 224 if (!m.matches()) { 225 continue; 226 } 227 SingleEntry singleEntry = new SingleEntry(); 228 singleEntry.type = m.group(4) + ""; 229 singleEntry.name = m.group(5); 230 singleEntry.value = m.group(6); 231 singleEntry.extra = m.group(7) + ""; 232 entry = singleEntry; 233 if (DEBUG) System.out.println(prefix + " -- " + line); 234 } 235 236 out.children.add(entry); 237 } 238 } catch (RuntimeException e) { 239 LOGGER.warning("Error at line " + line); 240 throw e; 241 } 242 } 243 244 private static class Entry { 245 String type; 246 String name; 247 javaType()248 public String javaType() { 249 switch (type) { 250 case "bool": 251 return "boolean"; 252 case "int32": 253 return "int"; 254 case "int64": 255 return "long"; 256 case "float": 257 return "float"; 258 case "string": 259 return "String"; 260 default: 261 return "Object"; 262 } 263 } 264 265 /** 266 * Convert {@code name} from lower_underscore_case to lowerCamelCase. 267 * 268 * The equivalent in guava would be {@code LOWER_UNDERSCORE.to(LOWER_CAMEL, name)}, but to 269 * keep the build system simple we don't want to depend on guava. 270 */ javaName()271 public String javaName() { 272 if (name.length() == 0) { 273 return ""; 274 } 275 StringBuilder sb = new StringBuilder(name.length()); 276 sb.append(name.charAt(0)); 277 boolean upperCaseNext = false; 278 for (int i = 1; i < name.length(); i++) { 279 char c = name.charAt(i); 280 if (c == '_') { 281 upperCaseNext = true; 282 } else { 283 if (upperCaseNext) { 284 c = Character.toUpperCase(c); 285 } 286 sb.append(c); 287 upperCaseNext = false; 288 } 289 } 290 return sb.toString(); 291 } 292 293 @Override toString()294 public String toString() { 295 return name + ":" + type; 296 } 297 } 298 299 private static class SingleEntry extends Entry { 300 String value; 301 String extra; 302 writeTo(String prefix, PrintStream output)303 public void writeTo(String prefix, PrintStream output) { 304 output.println(" public static final int " 305 + prefix + name.toUpperCase() + " = " + value + ";"); 306 } 307 constructStatsEventWriter(String builderName, GroupEntry g)308 public String constructStatsEventWriter(String builderName, GroupEntry g) { 309 switch (type) { 310 case "bool": 311 return String.format("%s.writeBoolean(%s);", builderName, javaName()); 312 case "int32": 313 return String.format("%s.writeInt(%s);", builderName, javaName()); 314 case "int64": 315 return String.format("%s.writeLong(%s);", builderName, javaName()); 316 case "float": 317 return String.format("%s.writeFloat(%s);", builderName, javaName()); 318 case "string": 319 return String.format("%s.writeString(%s);", builderName, javaName()); 320 default: 321 LOGGER.warning("Type not found " + type + " " + g.name); 322 return ";"; 323 } 324 } 325 } 326 327 private static class GroupEntry extends Entry { 328 final HashMap<String, GroupEntry> imports; 329 final GroupEntry root; 330 final ArrayList<Entry> children = new ArrayList<>(); 331 332 String javaPackage = ""; 333 GroupEntry(GroupEntry root)334 public GroupEntry(GroupEntry root) { 335 if (root == null) { 336 this.root = this; 337 this.imports = new HashMap<>(); 338 } else { 339 this.root = root; 340 this.imports = root.imports; 341 } 342 } 343 findGroup(String name)344 public GroupEntry findGroup(String name) { 345 for (Entry e : children) { 346 if (e.name.equalsIgnoreCase(name)) { 347 return (GroupEntry) e; 348 } 349 } 350 if (root != this) { 351 GroupEntry e = root.findGroup(name); 352 if (e != null) { 353 return e; 354 } 355 } 356 if (name.indexOf(".") >= 0) { 357 // Look in imports 358 String pkg = name.substring(0, name.lastIndexOf(".") + 1); 359 String key = name.substring(name.lastIndexOf(".") + 1); 360 361 GroupEntry imp = imports.get(pkg); 362 if (imp != null) { 363 return imp.findGroup(key); 364 } 365 // Try import with a subclass packageName 366 if (javaPackage != null) { 367 imp = imports.get(javaPackage + pkg); 368 if (imp != null) { 369 return imp.findGroup(key); 370 } 371 } 372 } 373 return null; 374 } 375 getSingles()376 public List<SingleEntry> getSingles() { 377 List<SingleEntry> result = new ArrayList<>(); 378 for (Entry e : children) { 379 if (e instanceof SingleEntry) { 380 result.add((SingleEntry) e); 381 } 382 } 383 return result; 384 } 385 constructBuildStatsEventMethod()386 public String constructBuildStatsEventMethod() { 387 StringJoiner responseBuilder = new StringJoiner("\n"); 388 responseBuilder.add(" // Placeholder code for local development only"); 389 StringJoiner argBuilder = new StringJoiner(", "); 390 getSingles().forEach(entry -> argBuilder.add( 391 entry.javaType() + " " + entry.javaName())); 392 393 394 String signature = String.format( 395 " public static StatsEvent buildStatsEvent(int code, %s){", argBuilder); 396 397 responseBuilder.add(signature) 398 .add(" final StatsEvent.Builder builder = StatsEvent.newBuilder();") 399 .add(" builder.setAtomId(code);"); 400 getSingles().stream().map( 401 entry -> entry.constructStatsEventWriter(" builder",this)).forEach( 402 responseBuilder::add); 403 404 return responseBuilder.add(" return builder.build();") 405 .add(" }").toString(); 406 } 407 } 408 409 private static class MyReader implements Closeable { 410 411 final List<String> outImports; 412 final BufferedReader reader; 413 414 String rootPrefix = ""; 415 String javaPackage = ""; 416 String javaOuterClassName = ""; 417 boolean javaMultipleFiles = false; 418 419 boolean started = false; 420 boolean finished = false; 421 MyReader(BufferedReader reader, List<String> outImport)422 MyReader(BufferedReader reader, List<String> outImport) { 423 this.reader = reader; 424 this.outImports = outImport; 425 } 426 extractQuotes(String line)427 private String extractQuotes(String line) { 428 Pattern p = Pattern.compile("\"([^\"]*)\""); 429 Matcher m = p.matcher(line); 430 return m.find() ? m.group(1) : ""; 431 } 432 parseHeaders()433 private String parseHeaders() throws IOException { 434 String line = getEntry(); 435 if (line.startsWith("message") || line.equals("}") 436 || line.startsWith("enum") || line.startsWith("extend")) { 437 return line; 438 } 439 if (line.startsWith("import")) { 440 String impSrc = extractQuotes(line); 441 if (!impSrc.isEmpty()) { 442 outImports.add(impSrc); 443 } 444 } else if (line.startsWith("option")) { 445 if (line.contains(" java_package ")) { 446 rootPrefix = extractQuotes(line) + "."; 447 javaPackage = rootPrefix; 448 } else if (line.contains(" java_outer_classname ")) { 449 javaOuterClassName = extractQuotes(line); 450 } else if (line.contains(" java_multiple_files ")) { 451 javaMultipleFiles = line.contains("true"); 452 } 453 } else if (line.startsWith("package")) { 454 rootPrefix = line.split(" ")[1].split(";")[0].trim() + "."; 455 javaPackage = rootPrefix; 456 } 457 return parseHeaders(); 458 } 459 onHeaderParseComplete()460 private void onHeaderParseComplete() { 461 if (!javaMultipleFiles && !javaOuterClassName.isEmpty()) { 462 rootPrefix = rootPrefix + javaOuterClassName + "."; 463 } 464 } 465 getEntry()466 String getEntry() throws IOException { 467 if (!started) { 468 started = true; 469 String entry = parseHeaders(); 470 onHeaderParseComplete(); 471 return entry; 472 } 473 String line = reader.readLine(); 474 475 if (line == null) { 476 // Finished everything 477 finished = true; 478 return "}"; 479 } 480 481 line = line.trim(); 482 483 // Skip comments 484 int commentIndex = line.indexOf("//"); 485 if (commentIndex > -1) { 486 line = line.substring(0, commentIndex).trim(); 487 } 488 489 if (line.startsWith("/*")) { 490 while (!line.contains("*/")) line = reader.readLine().trim(); 491 line = getEntry(); 492 } 493 494 if (!line.endsWith("{") && !line.endsWith(";") && !line.endsWith("}")) { 495 line = line + " " + getEntry(); 496 } 497 return line.trim(); 498 } 499 500 @Override close()501 public void close() throws IOException { 502 reader.close(); 503 } 504 } 505 } 506