EDIT:
Found an issue myself - the SwingWorker
s were not printing the tasks in the same order. I fixed it feeding just one SwingWorker
a list of reports and looping over it inside the doInBackground()
method. I am not sure about it's performance though. Anyone has any thoughts on it? New code for the ProcessAndPrintTask is:
class ProcessAndPrintTask extends SwingWorker<Void, Void> {
private List<Report> reports;
Integer reportResult;
ProcessAndPrintTask(List<Report> reports) {
this.reports = reports;
}
@Override
protected Void doInBackground() {
for (Report report : reports) {
try {
reportResult = report.getComparator().compareTwoFiles(new FileInputStream(new File(pathToReportsA + report.getFilename())),
new FileInputStream(new File(pathToReportsB + report.getFilename())));
} catch (IOException ex) {
ex.printStackTrace();
}
String message = report.getFilename() + ": ";
if (reportResult != null) {
switch (reportResult) {
case 1:
StyleConstants.setBackground(style, Color.GREEN);
try {
doc.insertString(doc.getLength(), message + "MATCH\n", style);
} catch (BadLocationException ex) {
ex.printStackTrace();
}
break;
case 0:
StyleConstants.setBackground(style, Color.RED);
try {
doc.insertString(doc.getLength(), message + "NO MATCH\n\n", style);
try {
for (String s : report.getComparator().getDifferences(
new FileInputStream(new File(pathToReportsA + report.getFilename())),
new FileInputStream(new File(pathToReportsB + report.getFilename())))) {
doc.insertString(doc.getLength(), s + "\n", style);
}
} catch (Exception ex) {
ex.printStackTrace();
}
} catch (BadLocationException ex) {
ex.printStackTrace();
}
break;
case -1:
StyleConstants.setBackground(style, Color.CYAN);
try {
doc.insertString(doc.getLength(), message + "BOTH FILES EMPTY\n", style);
} catch (BadLocationException ex) {
ex.printStackTrace();
}
break;
default:
StyleConstants.setBackground(style, Color.ORANGE);
try {
doc.insertString(doc.getLength(), message + "PROBLEM\n", style);
} catch (BadLocationException ex) {
ex.printStackTrace();
}
}
}
else {
StyleConstants.setBackground(style, Color.ORANGE);
try {
doc.insertString(doc.getLength(), message + "FILE OR FILES NOT FOUND\n", style);
}
catch (BadLocationException ex) {
ex.printStackTrace();
}
}
}
return null;
}
}
-------------------------------------------------------------------------
This is an app that loads an .xlsx list of report names, compares two instances of each report, prints the result (green - for a match, red - no match, blue - both files empty, yellow - some problem) and differences if any. I used XMLUnit, Apache POI and OpenCSV for accessing the files. It is my first app using Swing as well, so critique is most welcome. OK, this is gonna be a long one...
Report.java
public class Report {
private String name, format;
private ReportComparator comparator;
public Report(String name, String format) {
this.name = name;
this.format = format;
switch (format) {
case "xml":
this.comparator = new XMLReportComparator();
break;
case "csv":
this.comparator = new CSVReportComparator();
break;
case "xlsx":
this.comparator = new XLSXReportComparator();
break;
default:
throw new IllegalArgumentException("invalid format");
}
}
String getFilename() {
return this.name + "." + this.format;
}
ReportComparator getComparator() {
return this.comparator;
}
}
ReportComparator.java
public interface ReportComparator {
// compare files (1 - match, 0 - no match, -1 - both files empty)
int compareTwoFiles(InputStream fileA, InputStream fileB) throws IOException;
// gets a list of format-specific differences between two files
ArrayList<String> getDifferences(InputStream fileA, InputStream fileB) throws IOException;
}
XLSXReportComparator.java
public class XLSXReportComparator implements ReportComparator {
private String xlsxCellDifferenceFormat, xlsxRowDifferenceFormatA,
xlsxRowDifferenceFormatB, xlsxSheetDifferenceFormatA,
xlsxSheetDifferenceFormatB, xlsxFileDifferenceFormatA,
xlsxFileDifferenceFormatB;
XLSXReportComparator() {
Properties prop = new Properties();
String differencesFormats = "..\\resources\\differencesFormats.properties";
try (InputStream input = new FileInputStream(new File(differencesFormats))){
prop.load(input);
xlsxCellDifferenceFormat = prop.getProperty("xlsxCellDifferenceFormat");
xlsxRowDifferenceFormatA = prop.getProperty("xlsxRowDifferenceFormatA");
xlsxRowDifferenceFormatB = prop.getProperty("xlsxRowDifferenceFormatB");
xlsxSheetDifferenceFormatA = prop.getProperty("xlsxSheetDifferenceFormatA");
xlsxSheetDifferenceFormatB = prop.getProperty("xlsxSheetDifferenceFormatB");
xlsxFileDifferenceFormatA = prop.getProperty("xlsxFileDifferenceFormatA");
xlsxFileDifferenceFormatB = prop.getProperty("xlsxFileDifferenceFormatB");
}
catch (IOException ex) {
ex.printStackTrace();
}
}
@Override
public int compareTwoFiles(InputStream fileA, InputStream fileB) throws IOException{
Workbook workbookA= StreamingReader.builder()
.rowCacheSize(100) // number of rows to keep in memory (defaults to 10)
.bufferSize(4096) // buffer size to use when reading InputStream to file (defaults to 1024)
.open(fileA);
Workbook workbookB= StreamingReader.builder()
.rowCacheSize(100) // number of rows to keep in memory (defaults to 10)
.bufferSize(4096) // buffer size to use w
// hen reading InputStream to file (defaults to 1024)
.open(fileB);
Iterator<Sheet> sheetIteratorA = workbookA.iterator();
Iterator<Sheet> sheetIteratorB = workbookB.iterator();
// empty file flag
boolean empty = true;
while (sheetIteratorA.hasNext() && sheetIteratorB.hasNext()) {
Iterator<Row> rowIteratorA = sheetIteratorA.next().iterator();
Iterator<Row> rowIteratorB = sheetIteratorB.next().iterator();
while (rowIteratorA.hasNext() && rowIteratorB.hasNext()) {
Iterator<Cell> cellIteratorA = rowIteratorA.next().iterator();
Iterator<Cell> cellIteratorB = rowIteratorB.next().iterator();
while (cellIteratorA.hasNext() && cellIteratorB.hasNext()) {
Cell cellA = cellIteratorA.next();
Cell cellB = cellIteratorB.next();
// if the already checked part of file is empty, check if current cells are empty
if (empty) {
// if one of the cells isn't empty, change empty flag
if (!cellA.getStringCellValue().isEmpty() || !cellB.getStringCellValue().isEmpty())
empty = false;
}
// if values are different, return
if (!cellA.getStringCellValue().equals(cellB.getStringCellValue()))
return 0;
}
// check if any cells remain in either iterator, indicating uneven rows
if (cellIteratorA.hasNext() || cellIteratorB.hasNext())
return 0;
}
// check if any rows remain in either iterator, indicating uneven sheets
if (rowIteratorA.hasNext() || rowIteratorB.hasNext())
return 0;
}
// check if any sheets remain in either iterator, indicating uneven files
if (sheetIteratorA.hasNext() || sheetIteratorB.hasNext())
return 0;
if (empty) {
return -1;
}
return 1;
}
@Override
public ArrayList<String> getDifferences(InputStream fileA, InputStream fileB) throws IOException {
ArrayList<String> differences = new ArrayList<>();
Workbook workbookA= StreamingReader.builder()
.rowCacheSize(100) // number of rows to keep in memory (defaults to 10)
.bufferSize(4096) // buffer size to use when reading InputStream to file (defaults to 1024)
.open(fileA);
Workbook workbookB= StreamingReader.builder()
.rowCacheSize(100) // number of rows to keep in memory (defaults to 10)
.bufferSize(4096) // buffer size to use w
// hen reading InputStream to file (defaults to 1024)
.open(fileB);
Iterator<Sheet> sheetIteratorA = workbookA.iterator();
Iterator<Sheet> sheetIteratorB = workbookB.iterator();
while (sheetIteratorA.hasNext() && sheetIteratorB.hasNext()) {
Sheet currentSheetA = sheetIteratorA.next();
Sheet currentSheetB = sheetIteratorB.next();
Iterator<Row> rowIteratorA = currentSheetA.iterator();
Iterator<Row> rowIteratorB = currentSheetB.iterator();
while (rowIteratorA.hasNext() && rowIteratorB.hasNext()) {
Row currentRowA = rowIteratorA.next();
Row currentRowB = rowIteratorB.next();
Iterator<Cell> cellIteratorA = currentRowA.iterator();
Iterator<Cell> cellIteratorB = currentRowB.iterator();
while (cellIteratorA.hasNext() && cellIteratorB.hasNext()) {
Cell currentCellA = cellIteratorA.next();
Cell currentCellB = cellIteratorB.next();
// if values are different, add to list
if (!currentCellA.getStringCellValue().equals(currentCellB.getStringCellValue()))
differences.add(String.format(xlsxCellDifferenceFormat,
currentSheetA.getSheetName(),
currentRowA.getRowNum(),
currentCellA.getColumnIndex(),
currentCellA.getStringCellValue(),
currentCellB.getStringCellValue())
);
}
// check if any cells remain in either iterator, indicating uneven rows
if (cellIteratorA.hasNext())
differences.add(String.format(xlsxRowDifferenceFormatA,
currentSheetA.getSheetName(),
currentRowA.getRowNum())
);
if (cellIteratorB.hasNext())
differences.add(String.format(xlsxRowDifferenceFormatB,
currentSheetA.getSheetName(),
currentRowA.getRowNum())
);
}
// check if any rows remain in either iterator, indicating uneven sheets
if (rowIteratorA.hasNext())
differences.add(String.format(xlsxSheetDifferenceFormatA,
currentSheetA.getSheetName())
);
if (rowIteratorB.hasNext())
differences.add(String.format(xlsxSheetDifferenceFormatB,
currentSheetB.getSheetName())
);
}
// check if any sheets remain in either iterator, indicating sheet number
if (sheetIteratorA.hasNext())
differences.add(xlsxFileDifferenceFormatA);
if (sheetIteratorB.hasNext())
differences.add(xlsxFileDifferenceFormatB);
return differences;
}
}
XMLReportComparator.java
public class XMLReportComparator implements ReportComparator {
@Override
public int compareTwoFiles(InputStream fileA, InputStream fileB) throws IOException {
if (fileA.available() == 0 && fileB.available() == 0)
return -1;
Diff diff = DiffBuilder
.compare(fileA)
.withTest(fileB)
.withComparisonController(ComparisonControllers.StopWhenDifferent)
.build();
if (diff.hasDifferences())
return 0;
else
return 1;
}
@Override
public ArrayList<String> getDifferences(InputStream fileA, InputStream fileB) throws IOException {
ArrayList<String> differences = new ArrayList<>();
Diff diff = DiffBuilder.compare(fileA).withTest(fileB).build();
Iterator<Difference> iter = diff.getDifferences().iterator();
while (iter.hasNext()) {
differences.add(iter.next().toString());
}
return differences;
}
}
CSVReportComparator.java
public class CSVReportComparator implements ReportComparator {
private String csvCellDifferenceFormat, csvRowDifferenceFormatA,
csvRowDifferenceFormatB, csvFileDifferenceFormatA,
csvFileDifferenceFormatB;
CSVReportComparator() {
Properties p = new Properties();
String differencesFormats = "..\\resources\\differencesFormats.properties";
try (InputStream input = new FileInputStream(new File(differencesFormats))){
p.load(input);
csvCellDifferenceFormat = p.getProperty("csvCellDifferenceFormat");
csvRowDifferenceFormatA = p.getProperty("csvRowDifferenceFormatA");
csvRowDifferenceFormatB = p.getProperty("csvRowDifferenceFormatB");
csvFileDifferenceFormatA = p.getProperty("csvFileDifferenceFormatA");
csvFileDifferenceFormatB = p.getProperty("csvFileDifferenceFormatB");
}
catch (IOException ex) {
ex.printStackTrace();
}
}
@Override
public int compareTwoFiles(InputStream fileA, InputStream fileB) throws IOException {
CSVReader readerA = new CSVReader(new InputStreamReader(fileA));
CSVReader readerB = new CSVReader(new InputStreamReader(fileB));
// empty file flag
boolean empty = true;
Iterator<String[]> iteratorA = readerA.iterator();
Iterator<String[]> iteratorB = readerB.iterator();
while (iteratorA.hasNext() && iteratorB.hasNext()) {
String[] currentLineA = iteratorA.next();
String[] currentLineB = iteratorB.next();
// if lines length doesn't match - return 0
if (currentLineA.length != currentLineB.length) {
return 0;
}
else {
for (int index = 0; index < currentLineA.length; index++) {
// if the already checked part of file is empty, check if current cells are empty
if (empty) {
// if one of the fields isn't empty, change empty flag
if (!currentLineA[index].equals("") || !currentLineB[index].equals("")) {
empty = false;
}
}
// if fields don't match - return 0
if (!currentLineA[index].equals(currentLineB[index])) {
return 0;
}
}
}
}
if (iteratorA.hasNext() ^ iteratorB.hasNext()) {
return 0;
}
if (empty) {
return -1;
}
return 1;
}
@Override
public ArrayList<String> getDifferences(InputStream fileA, InputStream fileB) throws IOException {
ArrayList<String> differences = new ArrayList<>();
CSVReader readerA = new CSVReader(new InputStreamReader(fileA));
CSVReader readerB = new CSVReader(new InputStreamReader(fileB));
Iterator<String[]> iteratorA = readerA.iterator();
Iterator<String[]> iteratorB = readerB.iterator();
int iteratorIndex = 0;
while (iteratorA.hasNext() && iteratorB.hasNext()) {
String[] currentLineA = iteratorA.next();
String[] currentLineB = iteratorB.next();
// check whether rows have the same length
if (currentLineA.length > currentLineB.length) {
differences.add(String.format(csvRowDifferenceFormatA, iteratorIndex));
}
else if (currentLineA.length < currentLineB.length) {
differences.add(String.format(csvRowDifferenceFormatB, iteratorIndex));
}
// check for differences in fields even if lengths don't match
for (int index = 0; index < currentLineA.length && index < currentLineB.length; index++) {
// if fields don't match - add to list
if (!currentLineA[index].equals(currentLineB[index])) {
differences.add(String.format(csvCellDifferenceFormat, iteratorIndex, index, currentLineA[index], currentLineB[index]));
}
}
iteratorIndex++;
}
if (iteratorA.hasNext()) {
differences.add(csvFileDifferenceFormatA);
}
if (iteratorB.hasNext()) {
differences.add(csvFileDifferenceFormatB);
}
return differences;
}
}
GUI Client ReportTestGUI.java
public class ReportTestGUI extends JPanel
implements ActionListener {
private String reportListPath, pathToReportsA, pathToReportsB;
JButton chooseListButton, processReportsButton;
JTextPane messagePane;
JFileChooser fileChooser;
StyledDocument doc;
Style style;
class ProcessAndPrintTask extends SwingWorker<Void, Void> {
private Report report;
Integer reportResult;
public ProcessAndPrintTask(Report report) {
this.report = report;
reportResult = null;
}
@Override
protected Void doInBackground() {
try {
reportResult = report.getComparator().compareTwoFiles(new FileInputStream(new File(pathToReportsA + report.getFilename())),
new FileInputStream(new File(pathToReportsB + report.getFilename())));
}
catch (IOException ex) {
ex.printStackTrace();
}
return null;
}
@Override
protected void done() {
String message = report.getFilename() + ": ";
if (reportResult != null) {
// print result with style according to result code
switch (reportResult) {
case 1:
StyleConstants.setBackground(style, Color.GREEN);
try {
doc.insertString(doc.getLength(), message + "MATCH\n", style);
}
catch (BadLocationException ex) {}
break;
case 0:
StyleConstants.setBackground(style, Color.RED);
try {
doc.insertString(doc.getLength(), message + "NO MATCH\n\n", style);
try {
for (String s : report.getComparator().getDifferences(
new FileInputStream(new File(pathToReportsA + report.getFilename())),
new FileInputStream(new File(pathToReportsB + report.getFilename())))) {
doc.insertString(doc.getLength(), s + "\n", style);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
catch (BadLocationException ex) {}
break;
case -1:
StyleConstants.setBackground(style, Color.CYAN);
try {
doc.insertString(doc.getLength(), message + "BOTH FILES EMPTY\n", style);
}
catch (BadLocationException ex) {}
break;
default:
StyleConstants.setBackground(style, Color.ORANGE);
try {
doc.insertString(doc.getLength(), message + "PROBLEM\n", style);
}
catch (BadLocationException ex) {}
}
}
else {
StyleConstants.setBackground(style, Color.ORANGE);
try {
doc.insertString(doc.getLength(), message + "FILE OR FILES NOT FOUND\n", style);
}
catch (BadLocationException ex) {}
}
}
}
public ReportTestGUI() {
super(new BorderLayout());
messagePane = new JTextPane();
messagePane.setMargin(new Insets(5, 5, 5, 5));
messagePane.setEditable(false);
doc = messagePane.getStyledDocument();
style = messagePane.addStyle("Style", null);
JScrollPane messageScrollPane = new JScrollPane(messagePane);
fileChooser = new JFileChooser();
chooseListButton = new JButton("Choose report list");
processReportsButton = new JButton("Process records");
chooseListButton.addActionListener(this);
chooseListButton.setEnabled(false);
processReportsButton.addActionListener(this);
JPanel buttonPanel = new JPanel();
buttonPanel.add(chooseListButton);
buttonPanel.add(processReportsButton);
add(buttonPanel, BorderLayout.PAGE_START);
add(messageScrollPane, BorderLayout.CENTER);
loadDefaultData();
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == chooseListButton) {
int returnVal = fileChooser.showOpenDialog(ReportTestGUI.this);
if (returnVal == JFileChooser.APPROVE_OPTION) {
reportListPath = fileChooser.getSelectedFile().getAbsolutePath();
}
}
else if (e.getSource() == processReportsButton) {
StyleConstants.setBackground(style, Color.GREEN);
try (FileInputStream reportListExcelFile = new FileInputStream(new File(reportListPath))) {
Workbook workbook = new XSSFWorkbook(reportListExcelFile);
Sheet sheet = workbook.getSheetAt(0);
Iterator<Row> iter = sheet.iterator();
// skip first row that contains columns names
iter.next();
while (iter.hasNext()) {
Row r = iter.next();
String name = r.getCell(0).getStringCellValue();
String format = r.getCell(1).getStringCellValue();
Report currentReport = new Report(name, format);
new ProcessAndPrintTask(currentReport).execute();
}
}
catch (IOException exc) {
exc.printStackTrace();
}
}
}
private static void createAndShowGUI() {
JFrame mainFrame = new JFrame("Report Comparator");
mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainFrame.add(new ReportTestGUI());
mainFrame.pack();
mainFrame.setVisible(true);
mainFrame.setVisible(true);
}
private void loadDefaultData() {
Properties prop = new Properties();
String configFilePath = "..\\resources\\config.properties";
try (InputStream input = new FileInputStream(new File(configFilePath))) {
prop.load(input);
String resourcesPath = prop.getProperty("resourcesPath");
reportListPath = resourcesPath + prop.getProperty("reportListFilename");
pathToReportsA = prop.getProperty("pathToReportsA");
pathToReportsB = prop.getProperty("pathToReportsB");
}
catch (IOException e) {
e.printStackTrace();
}
}
public static void main (String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
}
2 Answers 2
I was looking at this
for (int index = 0; index < currentLineA.length; index++) { // if the already checked part of file is empty, check if current cells are empty if (empty) { // if one of the fields isn't empty, change empty flag if (!currentLineA[index].equals("") || !currentLineB[index].equals("")) { empty = false; } } // if fields don't match - return 0 if (!currentLineA[index].equals(currentLineB[index])) { return 0; } }
if both are equal to ""
then they would be equal, right? then we don't want to run the next if statement. we should add a continue
to that if statement so that we can go to the next cell and check it.
-
1\$\begingroup\$ Thank you, I missed that. It probably wouldn't be an issue for most of the real-life cases I am dealing with at the moment but if a file begins with a lot of empty fields, performance would suffer. \$\endgroup\$DCzo– DCzo2017年08月11日 08:29:48 +00:00Commented Aug 11, 2017 at 8:29
I did not read everything in detail.
You do the heavy work in a SwingWorker not to the block the UI thread which is good. You might have used JavaFX instead of Swing since the latter is more or less deprecated.
In Report
I would have made all members final
. I would have made the getters public
instead of package default; I'm not sure if there was a reason for that.
When you create an ArrayList
, it's better to define the variable as a List
and also the returned values. It's better to use the most general interface since you can then change the implementation by only modifying the point of creation without changing any other code. Also the code is easier to read if it's just List
, without wondering if there is some reason why it's an ArrayList
and not a List
.
-
\$\begingroup\$ Thank you for your insight! About the Report class - actually, the lack of access modifiers is not intentional, I simply forgot to add one ;). However, I seriously doubt that it is going to be used outside this application, so I don't know if making them public is necessary. What are your thoughts about it? \$\endgroup\$DCzo– DCzo2017年08月11日 08:27:05 +00:00Commented Aug 11, 2017 at 8:27
-
\$\begingroup\$ I would make them public. The reason might seem silly but it's just because it's easier to read. If it's not public, people will stop and think about why it's not public (that's what I did). Java should have made public the default. Kotlin tries to correct Java's mistakes and they made accessors public by default. \$\endgroup\$toto2– toto22017年08月11日 12:58:53 +00:00Commented Aug 11, 2017 at 12:58
-
\$\begingroup\$ That is not a silly reason at all - after all time is money :) \$\endgroup\$DCzo– DCzo2017年08月11日 13:42:46 +00:00Commented Aug 11, 2017 at 13:42