    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}