To download the tool and for usage instructions, see the follow up to this question
Description
I have realized that there's a whole bunch of code I want to have reviewed, but copying file by file, selecting the code and pressing Ctrl + K is a slow process, and I want to remember which other parts to include in my question, so I decided to make a tool for it. I thought it could be useful, especially since most of my questions are structured in very similar ways already...
This code is for inputting a couple of files, and outputting a CR question stub, ready to be filled in with details. The code automatically formats the input files to match StackExchange formatting, with four spaces added in front of each line (No more Ctrl + K !!)
It is very likely that you will see more questions from me structured in the same way as this question is. And if you want to use it yourself, feel free to do so.
Related previously existing tools:
- Tool for automatically correcting indentation and formatting of CR & SO code
- Bookmarklet for selecting code snippets on Code Review
Code download
For your convenience, this code can be found on GitHub (Many thanks to @amon who taught me a lot more about how to use git)
Class Summary (10079 bytes in 342 lines in 4 files)
- CountingStream: OutputStream that keeps track on the number of written bytes to it
- ReviewPrepareFrame: JFrame for letting user select files that should be up for review
- ReviewPreparer: The most important class, takes care of most of the work. Expects a List of files in the constructor and an OutputStream when called.
- TextAreaOutputStream: OutputStream for outputting to a
JTextArea
.
Code
CountingStream: (approximately 679 bytes in 27 lines)
/**
* An output stream that keeps track of how many bytes that has been written to it.
*/
public class CountingStream extends FilterOutputStream {
private final AtomicInteger bytesWritten;
public CountingStream(OutputStream out) {
super(out);
this.bytesWritten = new AtomicInteger();
}
@Override
public void write(int b) throws IOException {
bytesWritten.incrementAndGet();
super.write(b);
}
public int getBytesWritten() {
return bytesWritten.get();
}
}
ReviewPrepareFrame: (approximately 3178 bytes in 109 lines)
public class ReviewPrepareFrame extends JFrame {
private static final long serialVersionUID = 2050188992596669693L;
private JPanel contentPane;
private final JTextArea result = new JTextArea();
/**
* Launch the application.
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
new ReviewPrepareFrame().setVisible(true);
}
catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* Create the frame.
*/
public ReviewPrepareFrame() {
setTitle("Prepare code for Code Review");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(100, 100, 450, 300);
contentPane = new JPanel();
contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
contentPane.setLayout(new BorderLayout(0, 0));
setContentPane(contentPane);
JPanel panel = new JPanel();
contentPane.add(panel, BorderLayout.NORTH);
final DefaultListModel<File> model = new DefaultListModel<>();
final JList<File> list = new JList<File>();
panel.add(list);
list.setModel(model);
JButton btnAddFiles = new JButton("Add files");
btnAddFiles.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JFileChooser dialog = new JFileChooser();
dialog.setMultiSelectionEnabled(true);
if (dialog.showOpenDialog(ReviewPrepareFrame.this) == JFileChooser.APPROVE_OPTION) {
for (File file : dialog.getSelectedFiles()) {
model.addElement(file);
}
}
}
});
panel.add(btnAddFiles);
JButton btnRemoveFiles = new JButton("Remove files");
btnRemoveFiles.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
for (File file : new ArrayList<>(list.getSelectedValuesList())) {
model.removeElement(file);
}
}
});
panel.add(btnRemoveFiles);
JButton performButton = new JButton("Create Question stub with code included");
performButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
result.setText("");
ReviewPreparer preparer = new ReviewPreparer(filesToList(model));
TextAreaOutputStream outputStream = new TextAreaOutputStream(result);
preparer.createFormattedQuestion(outputStream);
}
});
contentPane.add(performButton, BorderLayout.SOUTH);
contentPane.add(result, BorderLayout.CENTER);
}
public List<File> filesToList(DefaultListModel<File> model) {
List<File> files = new ArrayList<>();
for (int i = 0; i < model.getSize(); i++) {
files.add(model.get(i));
}
return files;
}
}
ReviewPreparer: (approximately 5416 bytes in 165 lines)
public class ReviewPreparer {
private final List<File> files;
public ReviewPreparer(List<File> files) {
this.files = new ArrayList<>();
for (File file : files) {
if (file.getName().lastIndexOf('.') == -1)
continue;
if (file.length() < 10)
continue;
this.files.add(file);
}
}
public int createFormattedQuestion(OutputStream out) {
CountingStream counter = new CountingStream(out);
PrintStream ps = new PrintStream(counter);
outputHeader(ps);
outputFileNames(ps);
outputFileContents(ps);
outputDependencies(ps);
outputFooter(ps);
ps.print("Question Length: ");
ps.println(counter.getBytesWritten());
return counter.getBytesWritten();
}
private void outputFooter(PrintStream ps) {
ps.println("#Usage / Test");
ps.println();
ps.println();
ps.println("#Questions");
ps.println();
ps.println();
ps.println();
}
private void outputDependencies(PrintStream ps) {
List<String> dependencies = new ArrayList<>();
for (File file : files) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
String line;
while ((line = in.readLine()) != null) {
if (!line.startsWith("import ")) continue;
if (line.startsWith("import java.")) continue;
if (line.startsWith("import javax.")) continue;
String importStatement = line.substring("import ".length());
importStatement = importStatement.substring(0, importStatement.length() - 1); // cut the semicolon
dependencies.add(importStatement);
}
}
catch (IOException e) {
ps.println("Could not read " + file.getAbsolutePath());
ps.println();
// this will be handled by another function
}
}
if (!dependencies.isEmpty()) {
ps.println("#Dependencies");
ps.println();
for (String str : dependencies)
ps.println("- " + str + ": ");
}
ps.println();
}
private int countLines(File file) throws IOException {
return Files.readAllLines(file.toPath(), StandardCharsets.UTF_8).size();
}
private void outputFileContents(PrintStream ps) {
ps.println();
for (File file : files) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
ps.printf("**%s:** (approximately %d bytes in %d lines)", className(file), file.length(), countLines(file));
ps.println();
ps.println();
String line;
int importStatementsFinished = 0;
while ((line = in.readLine()) != null) {
// skip package and import declarations
if (line.startsWith("package ")) continue;
if (line.startsWith("import ")) {
importStatementsFinished = 1;
continue;
}
if (importStatementsFinished >= 0) importStatementsFinished = -1;
if (importStatementsFinished == -1 && line.trim().isEmpty()) continue;
importStatementsFinished = -2;
line = line.replaceAll(" ", "\t"); // replace four spaces with tabs, since that takes less space
ps.print(" "); // format as code for StackExchange, this needs to be spaces.
ps.println(line);
}
}
catch (IOException e) {
ps.print("> ");
e.printStackTrace(ps);
}
ps.println();
}
}
private void outputFileNames(PrintStream ps) {
int totalLength = 0;
int totalLines = 0;
for (File file : files) {
totalLength += file.length();
try {
totalLines += countLines(file);
}
catch (IOException e) {
ps.println("Unable to determine line count for " + file.getAbsolutePath());
}
}
ps.printf("###Class Summary (%d bytes in %d lines in %d files)", totalLength, totalLines, files.size());
ps.println();
ps.println();
for (File file : files) {
ps.println("- " + className(file) + ": ");
}
}
private String className(File file) {
String str = file.getName();
return str.substring(0, str.lastIndexOf('.'));
}
private void outputHeader(PrintStream ps) {
ps.println("#Description");
ps.println();
ps.println("- Add some [description for what the code does](http://meta.codereview.stackexchange.com/questions/1226/code-should-include-a-description-of-what-the-code-does)");
ps.println("- Is this a follow-up question? Answer [What has changed, Which question was the previous one, and why you are looking for another review](http://meta.codereview.stackexchange.com/questions/1065/how-to-post-a-follow-up-question)");
ps.println();
ps.println("#Code download");
ps.println();
ps.println("For convenience, this code can be downloaded from [somewhere](http://github.com repository perhaps?)");
ps.println();
}
public static void main(String[] args) throws IOException {
// List<File> fileList = Arrays.asList(new File("C:/_zomisnet/_reviewtest").listFiles());
List<File> fileList = Arrays.asList(new File("./src/net/zomis/reviewprep").listFiles());
new ReviewPreparer(fileList).createFormattedQuestion(System.out);
}
}
TextAreaOutputStream: (approximately 806 bytes in 41 lines)
public class TextAreaOutputStream extends OutputStream {
private final JTextArea textArea;
private final StringBuilder sb = new StringBuilder();
public TextAreaOutputStream(final JTextArea textArea) {
this.textArea = textArea;
}
@Override
public void flush() {
}
@Override
public void close() {
}
@Override
public void write(int b) throws IOException {
if (b == '\n') {
final String text = sb.toString() + "\n";
SwingUtilities.invokeLater(new Runnable() {
public void run() {
textArea.append(text);
}
});
sb.setLength(0);
return;
}
sb.append((char) b);
}
}
Usage / Test
Input: Run the JFrame, select files that should be reviewed, click the button.
Output: The stub to this question I am currently writing :)
Questions
Looking for general comments on my code. The code is Open-Source in case you want to continue the work I have started. I am currently only treating Java specially since that's what I code the most in, but I believe better support for other languages can be added later (I am not asking for how to do it, or for you to do it, I'm just saying that with some modifications, I believe it is possible).