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.IOException;
019import java.io.InputStream;
020import java.io.InputStreamReader;
021import java.io.OutputStream;
022import java.nio.file.Files;
023import java.nio.file.Path;
024import java.nio.file.Paths;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Comparator;
028import java.util.Formatter;
029import java.util.List;
030import java.util.Optional;
031import java.util.Properties;
032import java.util.concurrent.atomic.AtomicInteger;
033import java.util.stream.Collectors;
034
035import org.apache.tapestry5.versionmigrator.internal.ArtifactChangeRefactorCommitParser;
036import org.apache.tapestry5.versionmigrator.internal.PackageAndArtifactChangeRefactorCommitParser;
037import org.apache.tapestry5.versionmigrator.internal.PackageChangeRefactorCommitParser;
038
039public class Main 
040{
041
042    public static void main(String[] args) 
043    {
044        if (args.length == 0)
045        {
046            printHelp();
047        }
048        else 
049        {
050            TapestryVersion version = getTapestryVersion(args[1]);
051            switch (args[0])
052            {
053                case "generate": 
054                    createVersionFile(version);
055                    break;
056
057                case "upgrade": 
058                    upgrade(version);
059                    break;
060                    
061                default:
062                    printHelp();
063            }
064        }
065    }
066    
067    private static void upgrade(TapestryVersion version) 
068    {
069        
070        String path = "/" + getFileRelativePath(getSimpleFileName(version));
071        Properties properties = new Properties();
072        try (InputStream inputStream = Main.class.getResourceAsStream(path))
073        {
074            properties.load(inputStream);
075        } catch (IOException e) {
076            throw new RuntimeException(e);
077        }
078
079        List<File> sourceFiles = getJavaFiles();
080        
081        System.out.println("Number of renamed or moved classes: " + properties.size());
082        System.out.println("Number of source files found: " + sourceFiles.size());
083        
084        int totalCount = 0;
085        int totalChanged = 0;
086        for (File file : sourceFiles) 
087        {
088            boolean changed = upgrade(file, properties);
089            if (changed) {
090                totalChanged++;
091                try {
092                    System.out.println("Changed and upgraded file " + file.getCanonicalPath());
093                } catch (IOException e) {
094                    throw new RuntimeException(e);
095                }
096            }
097            totalCount++;
098            if (totalCount % 100 == 0)
099            {
100                System.out.printf("Processed %5d out of %d files (%.1f%%)\n", 
101                        totalCount, sourceFiles.size(), totalCount * 100.0 / sourceFiles.size());
102            }
103        }
104        
105        System.out.printf("Upgrade finished successfully. %s files changed out of %s.", totalChanged, totalCount);
106        
107    }
108    
109    private static boolean upgrade(File file, Properties properties)
110    {
111        Path path = Paths.get(file.toURI());
112        String content;
113        boolean changed = false;
114        try {
115            content = new String(Files.readAllBytes(path));
116            String newContent = content;
117            String newClassName;
118            for (String oldClassName : properties.stringPropertyNames()) 
119            {
120                newClassName = properties.getProperty(oldClassName);
121                newContent = newContent.replace(oldClassName, newClassName);
122            }
123            if (!newContent.equals(content))
124            {
125                changed = true;
126                Files.write(path, newContent.getBytes());
127            }
128        } catch (IOException e) {
129            throw new RuntimeException(e);
130        }
131        return changed;
132    }
133    
134    private static List<File> getJavaFiles() 
135    {
136        ArrayList<File> files = new ArrayList<>();
137        collectJavaFiles(new File("."), files);
138        return files;
139    }
140    
141    private static void collectJavaFiles(File currentFolder, List<File> javaFiles) 
142    {
143        File[] javaFilesInFolder = currentFolder.listFiles((f) -> f.isFile() && (f.getName().endsWith(".java") || f.getName().endsWith(".groovy")));
144        for (File file : javaFilesInFolder) {
145            javaFiles.add(file);
146        }
147        File[] subfolders = currentFolder.listFiles((f) -> f.isDirectory());
148        for (File subfolder : subfolders) {
149            collectJavaFiles(subfolder, javaFiles);
150        }
151    }
152
153    private static void printHelp() 
154    {
155        System.out.println("Apache Tapestry version migrator options:");
156        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.");
157        System.out.println("\t generate [version number]: analyzes version control and outputs information about moved classes.");
158        System.out.println("Apache Tapestry versions available in this tool: " + 
159                Arrays.stream(TapestryVersion.values())
160                    .map(TapestryVersion::getNumber)
161                    .collect(Collectors.joining(", ")));
162    }
163
164    private static TapestryVersion getTapestryVersion(String versionNumber) {
165        final TapestryVersion tapestryVersion = Arrays.stream(TapestryVersion.values())
166            .filter(v -> versionNumber.equals(v.getNumber()))
167            .findFirst()
168            .orElseThrow(() -> new IllegalArgumentException("Unknown Tapestry version: " + versionNumber + ". "));
169        return tapestryVersion;
170    }
171    
172    private static void createVersionFile(TapestryVersion version) 
173    {
174        final String commandLine = String.format("git diff --summary %s %s", 
175                version.getPreviousVersionGitHash(), version.getVersionGitHash());
176        final Process process;
177        
178        System.out.printf("Running command line '%s'\n", commandLine);
179        List<String> lines = new ArrayList<>();
180        try 
181        {
182            process = Runtime.getRuntime().exec(commandLine);
183        } catch (IOException e) {
184            throw new RuntimeException(e);
185        }
186        try (
187            final InputStream inputStream = process.getInputStream();
188            final InputStreamReader isr = new InputStreamReader(inputStream);
189            final BufferedReader reader = new BufferedReader(isr)) 
190        {
191            String line = reader.readLine();
192            while (line != null)
193            {
194                lines.add(line);
195                line = reader.readLine();
196            }
197        } catch (IOException e) {
198            throw new RuntimeException(e);
199        }
200        List<ClassRefactor> refactors = parse(lines);
201        AtomicInteger packageChange = new AtomicInteger();
202        AtomicInteger artifactChange = new AtomicInteger();
203        AtomicInteger packageAndArtifactChange = new AtomicInteger();
204        
205        refactors.stream().forEach(r -> {
206            if (r.isMovedBetweenArtifacts() && r.isRenamed()) {
207                packageAndArtifactChange.incrementAndGet();
208            }
209            if (r.isMovedBetweenArtifacts()) {
210                artifactChange.incrementAndGet();
211            }
212            if (r.isRenamed()) {
213                packageChange.incrementAndGet();
214            }
215        });
216        
217        System.out.println("Stats:");
218        System.out.printf("\t%d classes changed package or artifact\n", refactors.size()); 
219        System.out.printf("\t%d classes changed packages\n", packageChange.get()); 
220        System.out.printf("\t%d classes changed artifacts\n", artifactChange.get()); 
221        System.out.printf("\t%d classes changed both package and artifact\n", packageAndArtifactChange.get()); 
222        
223        writeVersionFile(version, refactors);
224        writeRefactorsFile(version, refactors);
225    }
226    
227    private static void writeRefactorsFile(TapestryVersion version, List<ClassRefactor> refactors) 
228    {
229        File file = getFile("change-report-" + version.getNumber() + ".html");
230        List<ClassRefactor> sorted = new ArrayList<>(refactors);
231        sorted.sort(Comparator.comparing(
232                ClassRefactor::isInternal).thenComparing(
233                        ClassRefactor::getSimpleOldClassName));
234        try (Formatter formatter = new Formatter(file))
235        {
236            formatter.format("<html>");
237            formatter.format("\t<head>");
238            formatter.format("\t\t<title>Changes introduced in Apache Tapestry %s</title>", version.getNumber());
239            formatter.format("\t</head>");
240            formatter.format("\t<body>");
241            formatter.format("\t\t<table>");
242            formatter.format("\t\t\t<thead>");
243            formatter.format("\t\t\t\t<th>Old class name</th>");
244            formatter.format("\t\t\t\t<th>Renamed or moved?</th>");
245            formatter.format("\t\t\t\t<th>New package location</th>");
246            formatter.format("\t\t\t\t<th>Moved artifacts?</th>");
247            formatter.format("\t\t\t\t<th>Old artifact location</th>");            
248            formatter.format("\t\t\t\t<th>New artifact location</th>");
249            formatter.format("\t\t\t</thead>");
250            formatter.format("\t\t\t<tbody>");
251            sorted.stream().forEach(r -> {
252                formatter.format("\t\t\t\t<tr>");
253                formatter.format("\t\t\t\t\t<td>%s</td>", r.getSimpleOldClassName());
254                boolean renamed = r.isRenamed();
255                boolean movedBetweenArtifacts = r.isMovedBetweenArtifacts();
256                formatter.format("\t\t\t\t\t<td>%s</td>", renamed ? "yes" : "no");
257                formatter.format("\t\t\t\t\t<td>%s</td>", renamed ? r.getNewPackageName() : "");
258                formatter.format("\t\t\t\t\t<td>%s</td>", movedBetweenArtifacts ? "yes" : "no");
259                formatter.format("\t\t\t\t\t<td>%s</td>", movedBetweenArtifacts ? r.getSourceArtifact() : "");
260                formatter.format("\t\t\t\t\t<td>%s</td>", movedBetweenArtifacts ? r.getDestinationArtifact() : "");
261                formatter.format("\t\t\t\t\t</tr>");
262            });
263            formatter.format("\t\t\t</tbody>");
264            formatter.format("\t\t</table>");            
265            formatter.format("\t</body>");            
266            formatter.format("</html>");
267            System.out.println("Change report file successfully written to " + file.getAbsolutePath());
268        } catch (Exception e) {
269            throw new RuntimeException(e);
270        }
271    }
272
273    private static void writeVersionFile(TapestryVersion version, List<ClassRefactor> refactors) 
274    {
275        Properties properties = new Properties();
276        refactors.stream()
277            .filter(ClassRefactor::isRenamed)
278            .forEach(r -> properties.setProperty(r.getOldClassName(), r.getNewClassName()));
279        
280        final File file = getChangesFile(version);
281        try (
282                OutputStream outputStream = new FileOutputStream(file);
283                BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream))
284        {
285            properties.store(bufferedOutputStream, version.toString());
286        } catch (Exception e) {
287            throw new RuntimeException(e);
288        }
289        System.out.println("Version file successfully written to " + file.getAbsolutePath());
290    }
291
292    private static File getChangesFile(TapestryVersion version) {
293        String filename = getSimpleFileName(version);
294        final File file = getFile(filename);
295        return file;
296    }
297
298    private static String getSimpleFileName(TapestryVersion version) {
299        return version.getNumber() + ".properties";
300    }
301
302    private static File getFile(String filename) {
303        final String fileRelativePath = getFileRelativePath(filename);
304        final File file = new File("src/main/resources/" + fileRelativePath);
305        file.getParentFile().mkdirs();
306        return file;
307    }
308
309    private static String getFileRelativePath(String filename) {
310        final String fileRelativePath = 
311                Main.class.getPackage().getName().replace('.', '/')
312                + "/" + filename;
313        return fileRelativePath;
314    }
315
316    private static List<ClassRefactor> parse(List<String> lines) 
317    {
318        System.out.println("Lines to process: " + lines.size());
319        
320        lines = lines.stream()
321            .map(s -> s.trim())
322            .filter(s -> s.startsWith("rename"))
323            .filter(s -> !s.contains("test"))
324            .filter(s -> !s.contains("package-info"))
325            .filter(s -> !s.contains("/resources/"))
326            .filter(s -> !s.contains("/filtered-resources/"))            
327            .map(s -> s.replaceFirst("rename", "").trim())
328            .collect(Collectors.toList());
329        
330        List<ClassRefactor> refactors = new ArrayList<>(lines.size());
331
332        for (String line : lines) 
333        {
334            PackageAndArtifactChangeRefactorCommitParser packageAndArtifactParser = new PackageAndArtifactChangeRefactorCommitParser();
335            ArtifactChangeRefactorCommitParser artifactParser = new ArtifactChangeRefactorCommitParser();
336            PackageChangeRefactorCommitParser packageParser = new PackageChangeRefactorCommitParser();
337            Optional<ClassRefactor> maybeMove = packageAndArtifactParser.apply(line);
338            if (!maybeMove.isPresent()) {
339                maybeMove = packageParser.apply(line);
340            }
341            if (!maybeMove.isPresent()) {
342                maybeMove = artifactParser.apply(line);
343            }
344            ClassRefactor move = maybeMove.orElseThrow(() -> new RuntimeException("Commit not handled: " + line));
345            refactors.add(move);
346        }
347        
348        return refactors;
349        
350    }
351
352}