这是创建CodeReview问题的工具的后续问题。
已更改的内容包括:
已删除用一个选项卡替换四个空格,代码本身中的所有选项卡和所有空格现在都保持原样。
在输出中添加了文件扩展名。
我觉得数字和行的顺序切换代码行比字节数更有趣。
支持命令行参数直接处理目录或一堆文件,并支持通配符。如果使用目录或通配符,则未通过ASCII内容检查的文件将被跳过。如果您指定的文件中包含大量非ASCII内容,则无论如何都会对其进行处理。
由于我添加的内容最多,我要求再次审核,请参见以下问题。 br />
类摘要(4个文件中的413行,总计12134字节)
CountingStream.java:OutputStream跟踪要写入的字节数它
ReviewPrepareFrame.java:JFrame,用于让用户选择应进行审查的文件。
ReviewPreparer.java:最重要的类,负责大部分工作。
TextAreaOutputStream.java:用于输出到JTextArea的OutputStream。
代码
代码也可以在GitHub上找到
CountingStream.java:(27行,679字节)
/**
* 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.java:(112行,3255字节)
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) {
if (args.length == 0) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
new ReviewPrepareFrame().setVisible(true);
}
catch (Exception e) {
e.printStackTrace();
}
}
});
}
else ReviewPreparer.main(args);
}
/**
* 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.java:(233行,7394字节)
public class ReviewPreparer {
public static double detectAsciiness(File input) throws IOException {
if (input.length() == 0)
return 0;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(input)))) {
int read;
long asciis = 0;
char[] cbuf = new char[1024];
while ((read = reader.read(cbuf)) != -1) {
for (int i = 0; i < read; i++) {
char c = cbuf[i];
if (c <= 0x7f)
asciis++;
}
}
return asciis / (double) input.length();
}
}
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();
// more detailed handling of this exception 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("#Code");
ps.println();
ps.println("This code can also be downloaded from [somewhere](http://github.com repository perhaps?)");
ps.println();
for (File file : files) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
int lines = -1;
try {
lines = countLines(file);
}
catch (IOException e) {
}
ps.printf("**%s:** (%d lines, %d bytes)", file.getName(), lines, file.length());
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()) // skip empty lines directly after import statements
continue;
importStatementsFinished = -2;
ps.print(" "); // format as code for StackExchange, this needs to be four spaces.
ps.println(line);
}
}
catch (IOException e) {
ps.print("> Unable to read " + file + ": "); // use a block-quote for exceptions
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 lines in %d files, making a total of %d bytes)", totalLines, files.size(), totalLength);
ps.println();
ps.println();
for (File file : files) {
ps.println("- " + file.getName() + ": ");
}
ps.println();
}
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();
}
public static boolean isAsciiFile(File file) {
try {
return detectAsciiness(file) >= 0.99;
}
catch (IOException e) {
return true; // if an error occoured, we want it to be added to a list and the error shown in the output
}
}
public static void main(String[] args) {
List<File> files = new ArrayList<>();
if (args.length == 0)
files.addAll(fileList("."));
for (String arg : args) {
files.addAll(fileList(arg));
}
new ReviewPreparer(files).createFormattedQuestion(System.out);
}
public static List<File> fileList(String pattern) {
List<File> files = new ArrayList<>();
File file = new File(pattern);
if (file.exists()) {
if (file.isDirectory()) {
for (File f : file.listFiles())
if (!f.isDirectory() && isAsciiFile(f))
files.add(f);
}
else files.add(file);
}
else {
// extract path
int lastSeparator = pattern.lastIndexOf('\');
lastSeparator = Math.max(lastSeparator, pattern.lastIndexOf('/'));
String path = lastSeparator < 0 ? "." : pattern.substring(0, lastSeparator);
file = new File(path);
// path has been extracted, check if path exists
if (file.exists()) {
// create a regex for searching for files, such as *.java, Test*.java
String regex = lastSeparator < 0 ? pattern : pattern.substring(lastSeparator + 1);
regex = regex.replaceAll("\.", "\.").replaceAll("\?", ".?").replaceAll("\*", ".*");
for (File f : file.listFiles()) {
// loop through directory, skip directories and filenames that don't match the pattern
if (!f.isDirectory() && f.getName().matches(regex) && isAsciiFile(f)) {
files.add(f);
}
}
}
else System.out.println("Unable to find path " + file);
}
return files;
}
}
TextAreaOutputStream.java: (41行,806字节)
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);
}
}
用法/测试
您现在可以通过从GitHub下载jar文件直接使用该工具并使用以下选项之一运行它:
java -jar ReviewPrepare.jar
运行Swing表单,使您可以使用GUI选择文件。java -jar ReviewPrepare.jar .
在当前工作目录中运行该程序并输出到stdout。java -jar ReviewPrepare.jar . > out.txt
在Windows中运行该程序当前工作目录并输出到文件out.txt
(我用这个来创建此问题)java -jar ReviewPrepare.jar C:/some/path/*.java > out.txt
在指定目录中运行程序,匹配所有* .java文件并输出到文件out.txt
问题
我目前主要关心的是实现命令行参数的方式,是否可以更轻松地完成? (最好不要使用外部库,因为我希望我的代码尽可能独立,尽管也欢迎这样做的库建议)我错过了任何常见的文件模式参数吗?
我我还有点担心它的可扩展性,现在感觉根本无法扩展。如果有人想为Python / C#/ C ++ / etc方式添加自定义功能该怎么办。文件是格式化的吗?然后以我做过的方式对“扫描导入文件”进行硬编码并不十分理想。
当然也欢迎进行一般评论。
#1 楼
一般现在您的帖子如此整洁,答案也将变得更加整洁。
GUI Bugs
当我运行GUI,它不允许我从文件浏览器中选择目录。它也从“文档”目录开始,最好做以下两件事之一:
从当前目录开始
从上一个使用的目录开始(使用java.util.pefs.Preferences吗?)
您应该添加:
JFileChooser dialog = new JFileChooser();
dialog.setCurrentDirectory(".");
dialog.setMultiSelectionEnabled(true);
dialog.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
然后,您还应该支持从选择器。这将使GUI中的行为与命令行更加匹配。
第二个问题是JTextArea显示。它应该具有滚动条,以便您可以在复制/粘贴结果之前检查结果。在查看这些更改时,我发现您正在事件分配线程上执行所有文件IO ...这是一种不好的做法...。
我必须执行以下操作:
// add a scrollPane....
private final JScrollPane scrollPane = new JScrollPane(result);
......
// Inside the constructor:
final Runnable viewupdater = new Runnable() {
public void run() {
result.setText("");
ReviewPreparer preparer = new ReviewPreparer(filesToList(model));
TextAreaOutputStream outputStream = new TextAreaOutputStream(result);
preparer.createFormattedQuestion(outputStream);
outputStream.flush();
result.setCaretPosition(0);
}
};
JButton performButton = new JButton("Create Question stub with code included");
performButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
Thread worker = new Thread(viewupdater);
worker.setDaemon(true);
worker.start();
}
});
scrollPane.setAutoscrolls(true);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
contentPane.add(scrollPane, BorderLayout.CENTER);
contentPane.add(performButton, BorderLayout.SOUTH);
在进行此更改时,我注意到您没有对TextAreaOutputStream实例进行任何最佳实践关闭,并且我查看了TextAreaOutputStream代码,并且,这不是正确的解决方案。它正在为每个文件中的每一行创建一个新线程。。。应该删除整个类,并替换为:
final Runnable viewupdater = new Runnable() {
public void run() {
ReviewPreparer preparer = new ReviewPreparer(filesToList(model));
try (final StringWriter sw = new StringWriter()) {
preparer.createFormattedQuestion(sw);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
result.setText(sw.toString());
result.setCaretPosition(0);
}
});
}
}
};
请注意,上面的内容如何更改为使用Writer而不是OutputStream .....使用OutputStream用于文本数据的模型是损坏的....用于文本的读取器和写入器,以及用于二进制的流。
这是进入非GUI代码的好方法....
Core引擎
TextareaOutputStream使我意识到,除了埋在ReviewPreparer中的某些部分之外,您所有的方法都是基于流的。
PrintStream代码都应该全部用StringBuilder替换。..无论如何,您都限于CR帖子的大小,并且正在将数据累积到TextArea中。
这也是CountingOutputStream的有趣的赛格威。也不需要....您不是用它来计算文件大小,而是实际的帖子长度。应该用字符而不是字节来度量...。因此,它是一个损坏的类。摆脱掉它。
所以,也摆脱掉PrintStream。 PrintStream是一个同步类,并且比StringBuilder慢得多。将数据追加到StringBuilder还意味着您可以从StringBuilder获得字符长度,而不是从CountingOutputStream获得字节长度。
最后一个观察结果..
outputFileContents(PrintStream ps)
方法内部这样做: try (BufferedReader in = new BufferedReader(new InputStreamReader(
new FileInputStream(file)))) {
int lines = -1;
try {
lines = countLines(file);
} catch (IOException e) {
}
ps.printf("**%s:** (%d lines, %d bytes)", file.getName(),
lines, file.length());
ps.println();
ps.println();
String line;
int importStatementsFinished = 0;
while ((line = in.readLine()) != null) {
由于一些原因,它被打破了。...
首先,您不应该使用FileInputStream,但是FileReader。
其次,您具有支持方法
countLines(File)
:private int countLines(File file) throws IOException {
return Files.readAllLines(file.toPath(), StandardCharsets.UTF_8).size();
}
此方法再次完全读取文件... ..为什么不将上面的所有大代码替换为:
try {
List<String> filelines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
sb.append(String.format("**%s:** (%d lines, %d bytes)", file.getName(),
filelines.size(), file.length()));
sb.append("\n\n");
int importStatementsFinished = 0;
for (String line : filelines) {
// skip package and import declarations
这样省去了两次读取每个文件的麻烦...
走了,这已经足够了。
评论
StackExchange上是否有模板,如何构造问题/答案?