001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012package org.apache.tapestry5.versionmigrator; 013 014import java.io.BufferedOutputStream; 015import java.io.BufferedReader; 016import java.io.File; 017import java.io.FileOutputStream; 018import java.io.FileWriter; 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.InputStreamReader; 022import java.io.OutputStream; 023import java.nio.file.Files; 024import java.nio.file.Path; 025import java.nio.file.Paths; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.Comparator; 029import java.util.Formatter; 030import java.util.HashSet; 031import java.util.Iterator; 032import java.util.List; 033import java.util.Optional; 034import java.util.Properties; 035import java.util.Set; 036import java.util.concurrent.atomic.AtomicInteger; 037import java.util.stream.Collectors; 038 039import javax.xml.namespace.NamespaceContext; 040import javax.xml.parsers.DocumentBuilder; 041import javax.xml.parsers.DocumentBuilderFactory; 042import javax.xml.transform.Transformer; 043import javax.xml.transform.TransformerConfigurationException; 044import javax.xml.transform.TransformerException; 045import javax.xml.transform.TransformerFactory; 046import javax.xml.transform.TransformerFactoryConfigurationError; 047import javax.xml.transform.dom.DOMSource; 048import javax.xml.transform.stream.StreamResult; 049import javax.xml.xpath.XPath; 050import javax.xml.xpath.XPathConstants; 051import javax.xml.xpath.XPathExpression; 052import javax.xml.xpath.XPathFactory; 053 054import org.apache.tapestry5.versionmigrator.internal.ArtifactChangeRefactorCommitParser; 055import org.apache.tapestry5.versionmigrator.internal.PackageAndArtifactChangeRefactorCommitParser; 056import org.apache.tapestry5.versionmigrator.internal.PackageChangeRefactorCommitParser; 057import org.w3c.dom.Document; 058import org.w3c.dom.Element; 059import org.w3c.dom.NodeList; 060 061public class Main 062{ 063 064 public static void main(String[] args) 065 { 066 if (args.length == 0) 067 { 068 printHelp(); 069 } 070 else 071 { 072 switch (args[0]) 073 { 074 case "artifactSuffix": 075 artifactSuffix(args); 076 break; 077 078 case "generate": 079 createVersionFile(getTapestryVersion(args[1])); 080 break; 081 082 case "upgrade": 083 upgrade(getTapestryVersion(args[1])); 084 break; 085 086 087 default: 088 printHelp(); 089 } 090 } 091 } 092 093 private static void artifactSuffix(final String[] args) 094 { 095 final String artifactSuffix = args.length == 2 ? args[1] : ""; 096 artifactSuffix(new File("."), artifactSuffix); 097 } 098 099 private static void artifactSuffix(final File file, final String artifactSuffix) 100 { 101 if (file.getName().equals("pom.xml")) 102 { 103 try 104 { 105 introduceArtifactSuffix(file, artifactSuffix); 106 } catch (Exception e) 107 { 108 throw new RuntimeException(e); 109 } 110 } 111 else 112 { 113 if (file.isDirectory()) 114 { 115 for (File f : file.listFiles()) 116 { 117 artifactSuffix(f, artifactSuffix); 118 } 119 } 120 } 121 } 122 123 private static final String MAVEN_NAMESPACE = "http://maven.apache.org/POM/4.0.0"; 124 125 private static final String SUFFIX_PROPERTY = "tapestry-artifact-suffix"; 126 127 private static final Set<String> SUFFIXED_ARTIFACTS = new HashSet<>(Arrays.asList( 128 "tapestry-core", "tapestry-http", "tapestry-test", 129 "tapestry-runner", "tapestry-spring", "tapestry-kaptcha", 130 "tapestry-openapi-viewer", "tapestry-upload", "tapestry-jmx", 131 "tapestry-jpa", "tapestry-kaptcha", "tapestry-openapi-viewer", 132 "tapestry-rest-jackson", "tapestry-webresources", "tapestry-cdi", 133 "tapestry-ioc", "tapestry-ioc-jcache", "tapestry-jmx", "tapestry-spock", 134 "tapestry-clojure", "tapestry-hibernate", "tapestry-hibernate-core", 135 "tapestry-ioc-junit", "tapestry-latest-java-tests", "tapestry-mongodb", 136 "tapestry-spock")); 137 138 private static final XPath XPATH; 139 140 static 141 { 142 XPATH = XPathFactory.newInstance().newXPath(); 143 XPATH.setNamespaceContext(new MavenNamespaceContext()); 144 } 145 146 private static void introduceArtifactSuffix(File file, String artifactSuffix) throws Exception 147 { 148 DocumentBuilderFactory f = DocumentBuilderFactory.newInstance(); 149 f.setNamespaceAware(true); 150 DocumentBuilder b = f.newDocumentBuilder(); 151 Document doc = b.parse(file); 152 153 boolean fileChanged = false; 154 155 final XPathExpression propertiesXpath = 156 XPATH.compile("/maven:project/maven:properties"); 157 Element properties = (Element) propertiesXpath.evaluate(doc, XPathConstants.NODE); 158 159 if (properties == null) 160 { 161 properties = doc.createElementNS(MAVEN_NAMESPACE, "properties"); 162 properties.appendChild(doc.createTextNode("\t\n")); 163 doc.getDocumentElement().appendChild(properties); 164 fileChanged = true; 165 } 166 167 final XPathExpression propertyXpath = 168 XPATH.compile(String.format("/maven:project/maven:properties/*[local-name()='%s']", 169 SUFFIX_PROPERTY)); 170 Element property = (Element) propertyXpath.evaluate(doc, XPathConstants.NODE); 171 if (property == null) 172 { 173 property = doc.createElementNS(MAVEN_NAMESPACE, SUFFIX_PROPERTY); 174 property.setTextContent(artifactSuffix); 175 properties.appendChild(doc.createTextNode("\t")); 176 properties.appendChild(doc.createComment(" Tapestry artifact id suffix (empty value or '-jakarta'). ")); 177 properties.appendChild(doc.createTextNode("\n\t\t")); 178 properties.appendChild(property); 179 properties.appendChild(doc.createTextNode("\n\t")); 180 fileChanged = true; 181 } 182 183 final XPathExpression artifactIdXpath = 184 XPATH.compile("/maven:project//maven:dependencies/maven:dependency/maven:artifactId"); 185 186 NodeList artifactIds = (NodeList) artifactIdXpath.evaluate(doc, XPathConstants.NODESET); 187 for (int i = 0; i < artifactIds.getLength(); i++) 188 { 189 Element artifactId = (Element) artifactIds.item(i); 190 final String value = artifactId.getTextContent(); 191 if (SUFFIXED_ARTIFACTS.contains(value)) 192 { 193 artifactId.setTextContent(value + "${" + SUFFIX_PROPERTY + "}"); 194 fileChanged = true; 195 } 196 } 197 198 if (fileChanged) 199 { 200 System.out.println("Updated " + file.getCanonicalPath()); 201 write(doc, file); 202 } 203 204 } 205 206 private static void write(Document doc, File file) 207 throws TransformerFactoryConfigurationError, TransformerConfigurationException, IOException, TransformerException 208 { 209 TransformerFactory transformerFactory = TransformerFactory.newInstance(); 210 Transformer transformer = transformerFactory.newTransformer(); 211 DOMSource source = new DOMSource(doc); 212 FileWriter writer = new FileWriter(file); 213 StreamResult streamResult = new StreamResult(writer); 214 transformer.transform(source, streamResult); 215 } 216 217 private static void upgrade(TapestryVersion version) 218 { 219 220 String path = "/" + getFileRelativePath(getSimpleFileName(version)); 221 Properties properties = new Properties(); 222 try (InputStream inputStream = Main.class.getResourceAsStream(path)) 223 { 224 properties.load(inputStream); 225 } catch (IOException e) { 226 throw new RuntimeException(e); 227 } 228 229 List<File> sourceFiles = getJavaFiles(); 230 231 System.out.println("Number of renamed or moved classes: " + properties.size()); 232 System.out.println("Number of source files found: " + sourceFiles.size()); 233 234 int totalCount = 0; 235 int totalChanged = 0; 236 for (File file : sourceFiles) 237 { 238 boolean changed = upgrade(file, properties); 239 if (changed) { 240 totalChanged++; 241 try { 242 System.out.println("Changed and upgraded file " + file.getCanonicalPath()); 243 } catch (IOException e) { 244 throw new RuntimeException(e); 245 } 246 } 247 totalCount++; 248 if (totalCount % 100 == 0) 249 { 250 System.out.printf("Processed %5d out of %d files (%.1f%%)\n", 251 totalCount, sourceFiles.size(), totalCount * 100.0 / sourceFiles.size()); 252 } 253 } 254 255 System.out.printf("Upgrade finished successfully. %s files changed out of %s.", totalChanged, totalCount); 256 257 } 258 259 private static boolean upgrade(File file, Properties properties) 260 { 261 Path path = Paths.get(file.toURI()); 262 String content; 263 boolean changed = false; 264 try { 265 content = new String(Files.readAllBytes(path)); 266 String newContent = content; 267 String newClassName; 268 for (String oldClassName : properties.stringPropertyNames()) 269 { 270 newClassName = properties.getProperty(oldClassName); 271 newContent = newContent.replace(oldClassName, newClassName); 272 } 273 if (!newContent.equals(content)) 274 { 275 changed = true; 276 Files.write(path, newContent.getBytes()); 277 } 278 } catch (IOException e) { 279 throw new RuntimeException(e); 280 } 281 return changed; 282 } 283 284 private static List<File> getJavaFiles() 285 { 286 ArrayList<File> files = new ArrayList<>(); 287 collectJavaFiles(new File("."), files); 288 return files; 289 } 290 291 private static void collectJavaFiles(File currentFolder, List<File> javaFiles) 292 { 293 File[] javaFilesInFolder = currentFolder.listFiles((f) -> f.isFile() && (f.getName().endsWith(".java") || f.getName().endsWith(".groovy"))); 294 for (File file : javaFilesInFolder) { 295 javaFiles.add(file); 296 } 297 File[] subfolders = currentFolder.listFiles((f) -> f.isDirectory()); 298 for (File subfolder : subfolders) { 299 collectJavaFiles(subfolder, javaFiles); 300 } 301 } 302 303 private static void printHelp() 304 { 305 System.out.println("Apache Tapestry version migrator options:"); 306 System.out.println("\t upgrade [version number]: updates references to classes which have been moved or renamed in Java source files in the current folder and its subfolders."); 307 System.out.println("\t generate [version number]: analyzes version control and outputs information about moved classes."); 308 System.out.println("\t artifactSuffix [suffix]: updates pom.xml files recursively to allow very easy changing from or to suffixed artifact ids (no suffix vs \"-jakarta\")."); 309 System.out.println("Apache Tapestry versions available in this tool: " + 310 Arrays.stream(TapestryVersion.values()) 311 .map(TapestryVersion::getNumber) 312 .collect(Collectors.joining(", "))); 313 } 314 315 private static TapestryVersion getTapestryVersion(String versionNumber) { 316 final TapestryVersion tapestryVersion = Arrays.stream(TapestryVersion.values()) 317 .filter(v -> versionNumber.equals(v.getNumber())) 318 .findFirst() 319 .orElseThrow(() -> new IllegalArgumentException("Unknown Tapestry version: " + versionNumber + ". ")); 320 return tapestryVersion; 321 } 322 323 private static void createVersionFile(TapestryVersion version) 324 { 325 final String commandLine = String.format("git diff --summary %s %s", 326 version.getPreviousVersionGitHash(), version.getVersionGitHash()); 327 final Process process; 328 329 System.out.printf("Running command line '%s'\n", commandLine); 330 List<String> lines = new ArrayList<>(); 331 try 332 { 333 process = Runtime.getRuntime().exec(commandLine); 334 } catch (IOException e) { 335 throw new RuntimeException(e); 336 } 337 try ( 338 final InputStream inputStream = process.getInputStream(); 339 final InputStreamReader isr = new InputStreamReader(inputStream); 340 final BufferedReader reader = new BufferedReader(isr)) 341 { 342 String line = reader.readLine(); 343 while (line != null) 344 { 345 lines.add(line); 346 line = reader.readLine(); 347 } 348 } catch (IOException e) { 349 throw new RuntimeException(e); 350 } 351 List<ClassRefactor> refactors = parse(lines); 352 AtomicInteger packageChange = new AtomicInteger(); 353 AtomicInteger artifactChange = new AtomicInteger(); 354 AtomicInteger packageAndArtifactChange = new AtomicInteger(); 355 356 refactors.stream().forEach(r -> { 357 if (r.isMovedBetweenArtifacts() && r.isRenamed()) { 358 packageAndArtifactChange.incrementAndGet(); 359 } 360 if (r.isMovedBetweenArtifacts()) { 361 artifactChange.incrementAndGet(); 362 } 363 if (r.isRenamed()) { 364 packageChange.incrementAndGet(); 365 } 366 }); 367 368 System.out.println("Stats:"); 369 System.out.printf("\t%d classes changed package or artifact\n", refactors.size()); 370 System.out.printf("\t%d classes changed packages\n", packageChange.get()); 371 System.out.printf("\t%d classes changed artifacts\n", artifactChange.get()); 372 System.out.printf("\t%d classes changed both package and artifact\n", packageAndArtifactChange.get()); 373 374 writeVersionFile(version, refactors); 375 writeRefactorsFile(version, refactors); 376 } 377 378 private static void writeRefactorsFile(TapestryVersion version, List<ClassRefactor> refactors) 379 { 380 File file = getFile("change-report-" + version.getNumber() + ".html"); 381 List<ClassRefactor> sorted = new ArrayList<>(refactors); 382 sorted.sort(Comparator.comparing( 383 ClassRefactor::isInternal).thenComparing( 384 ClassRefactor::getSimpleOldClassName)); 385 try (Formatter formatter = new Formatter(file)) 386 { 387 formatter.format("<html>"); 388 formatter.format("\t<head>"); 389 formatter.format("\t\t<title>Changes introduced in Apache Tapestry %s</title>", version.getNumber()); 390 formatter.format("\t</head>"); 391 formatter.format("\t<body>"); 392 formatter.format("\t\t<table>"); 393 formatter.format("\t\t\t<thead>"); 394 formatter.format("\t\t\t\t<th>Old class name</th>"); 395 formatter.format("\t\t\t\t<th>Renamed or moved?</th>"); 396 formatter.format("\t\t\t\t<th>New package location</th>"); 397 formatter.format("\t\t\t\t<th>Moved artifacts?</th>"); 398 formatter.format("\t\t\t\t<th>Old artifact location</th>"); 399 formatter.format("\t\t\t\t<th>New artifact location</th>"); 400 formatter.format("\t\t\t</thead>"); 401 formatter.format("\t\t\t<tbody>"); 402 sorted.stream().forEach(r -> { 403 formatter.format("\t\t\t\t<tr>"); 404 formatter.format("\t\t\t\t\t<td>%s</td>", r.getSimpleOldClassName()); 405 boolean renamed = r.isRenamed(); 406 boolean movedBetweenArtifacts = r.isMovedBetweenArtifacts(); 407 formatter.format("\t\t\t\t\t<td>%s</td>", renamed ? "yes" : "no"); 408 formatter.format("\t\t\t\t\t<td>%s</td>", renamed ? r.getNewPackageName() : ""); 409 formatter.format("\t\t\t\t\t<td>%s</td>", movedBetweenArtifacts ? "yes" : "no"); 410 formatter.format("\t\t\t\t\t<td>%s</td>", movedBetweenArtifacts ? r.getSourceArtifact() : ""); 411 formatter.format("\t\t\t\t\t<td>%s</td>", movedBetweenArtifacts ? r.getDestinationArtifact() : ""); 412 formatter.format("\t\t\t\t\t</tr>"); 413 }); 414 formatter.format("\t\t\t</tbody>"); 415 formatter.format("\t\t</table>"); 416 formatter.format("\t</body>"); 417 formatter.format("</html>"); 418 System.out.println("Change report file successfully written to " + file.getAbsolutePath()); 419 } catch (Exception e) { 420 throw new RuntimeException(e); 421 } 422 } 423 424 private static void writeVersionFile(TapestryVersion version, List<ClassRefactor> refactors) 425 { 426 Properties properties = new Properties(); 427 refactors.stream() 428 .filter(ClassRefactor::isRenamed) 429 .forEach(r -> properties.setProperty(r.getOldClassName(), r.getNewClassName())); 430 431 final File file = getChangesFile(version); 432 try ( 433 OutputStream outputStream = new FileOutputStream(file); 434 BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) 435 { 436 properties.store(bufferedOutputStream, version.toString()); 437 } catch (Exception e) { 438 throw new RuntimeException(e); 439 } 440 System.out.println("Version file successfully written to " + file.getAbsolutePath()); 441 } 442 443 private static File getChangesFile(TapestryVersion version) { 444 String filename = getSimpleFileName(version); 445 final File file = getFile(filename); 446 return file; 447 } 448 449 private static String getSimpleFileName(TapestryVersion version) { 450 return version.getNumber() + ".properties"; 451 } 452 453 private static File getFile(String filename) { 454 final String fileRelativePath = getFileRelativePath(filename); 455 final File file = new File("src/main/resources/" + fileRelativePath); 456 file.getParentFile().mkdirs(); 457 return file; 458 } 459 460 private static String getFileRelativePath(String filename) { 461 final String fileRelativePath = 462 Main.class.getPackage().getName().replace('.', '/') 463 + "/" + filename; 464 return fileRelativePath; 465 } 466 467 private static List<ClassRefactor> parse(List<String> lines) 468 { 469 System.out.println("Lines to process: " + lines.size()); 470 471 lines = lines.stream() 472 .map(s -> s.trim()) 473 .filter(s -> s.startsWith("rename")) 474 .filter(s -> !s.contains("test")) 475 .filter(s -> !s.contains("package-info")) 476 .filter(s -> !s.contains("/resources/")) 477 .filter(s -> !s.contains("/filtered-resources/")) 478 .map(s -> s.replaceFirst("rename", "").trim()) 479 .collect(Collectors.toList()); 480 481 List<ClassRefactor> refactors = new ArrayList<>(lines.size()); 482 483 for (String line : lines) 484 { 485 PackageAndArtifactChangeRefactorCommitParser packageAndArtifactParser = new PackageAndArtifactChangeRefactorCommitParser(); 486 ArtifactChangeRefactorCommitParser artifactParser = new ArtifactChangeRefactorCommitParser(); 487 PackageChangeRefactorCommitParser packageParser = new PackageChangeRefactorCommitParser(); 488 Optional<ClassRefactor> maybeMove = packageAndArtifactParser.apply(line); 489 if (!maybeMove.isPresent()) { 490 maybeMove = packageParser.apply(line); 491 } 492 if (!maybeMove.isPresent()) { 493 maybeMove = artifactParser.apply(line); 494 } 495 ClassRefactor move = maybeMove.orElseThrow(() -> new RuntimeException("Commit not handled: " + line)); 496 refactors.add(move); 497 } 498 499 return refactors; 500 501 } 502 503 private static final class MavenNamespaceContext implements NamespaceContext { 504 505 @Override 506 public Iterator<String> getPrefixes(String namespaceURI) 507 { 508 throw new UnsupportedOperationException(); 509 } 510 511 @Override 512 public String getPrefix(String namespaceURI) 513 { 514 throw new UnsupportedOperationException(); 515 } 516 517 @Override 518 public String getNamespaceURI(String prefix) 519 { 520 return prefix.equals("maven") ? MAVEN_NAMESPACE : null; 521 } 522 } 523 524}