diff --git a/build.gradle b/build.gradle index e251185..faf05e4 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ repositories { dependencies { testCompile group: 'junit', name: 'junit', version: '4.12' + testCompile group: 'org.hamcrest', name: 'hamcrest-library', version: '1.3' testCompile group: 'org.mockito', name: 'mockito-all', version: '1.10.19' compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.4.5' compile group: 'org.springframework', name:'spring-context', version: '4.2.3.RELEASE' diff --git a/src/main/java/ninja/javafx/smartcsv/fx/SmartCSVController.java b/src/main/java/ninja/javafx/smartcsv/fx/SmartCSVController.java index 94992d8..da2d60a 100644 --- a/src/main/java/ninja/javafx/smartcsv/fx/SmartCSVController.java +++ b/src/main/java/ninja/javafx/smartcsv/fx/SmartCSVController.java @@ -32,27 +32,26 @@ import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; -import javafx.scene.control.Alert; -import javafx.scene.control.Label; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; +import javafx.scene.control.*; +import javafx.scene.layout.AnchorPane; import javafx.scene.layout.BorderPane; import javafx.stage.FileChooser; import ninja.javafx.smartcsv.FileReader; import ninja.javafx.smartcsv.csv.CSVFileReader; import ninja.javafx.smartcsv.csv.CSVFileWriter; +import ninja.javafx.smartcsv.fx.list.ValidationErrorListCell; import ninja.javafx.smartcsv.fx.table.ObservableMapValueFactory; import ninja.javafx.smartcsv.fx.table.ValidationCellFactory; import ninja.javafx.smartcsv.fx.table.model.CSVModel; import ninja.javafx.smartcsv.fx.table.model.CSVValue; import ninja.javafx.smartcsv.fx.table.model.CSVRow; +import ninja.javafx.smartcsv.validation.ValidationError; import ninja.javafx.smartcsv.validation.ValidationFileReader; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.File; -import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; @@ -83,6 +82,12 @@ public class SmartCSVController extends FXMLController { @FXML private Label stateline; + @FXML + private ListView errorList; + + @FXML + private AnchorPane tableWrapper; + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // injections @@ -92,6 +97,7 @@ public class SmartCSVController extends FXMLController { private final LoadCSVService loadCSVService = new LoadCSVService(); private final SaveCSVService saveCSVService = new SaveCSVService(); private CSVModel model; + private TableView tableView; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -101,6 +107,10 @@ public class SmartCSVController extends FXMLController { @Override public void initialize(URL location, ResourceBundle resources) { stateline.setVisible(false); + errorList.setCellFactory(param -> new ValidationErrorListCell()); + errorList.getSelectionModel().selectedItemProperty().addListener( + observable -> scrollToError() + ); } @@ -215,7 +225,7 @@ public class SmartCSVController extends FXMLController { model = csvLoader.getData(); model.setValidator(validationLoader.getValidator()); - TableView tableView = new TableView<>(); + tableView = new TableView<>(); for (String column: model.getHeader()) { addColumn(column, tableView); @@ -223,7 +233,14 @@ public class SmartCSVController extends FXMLController { tableView.getItems().setAll(model.getRows()); tableView.setEditable(true); - applicationPane.setCenter(tableView); + AnchorPane.setBottomAnchor(tableView, 0.0); + AnchorPane.setTopAnchor(tableView, 0.0); + AnchorPane.setLeftAnchor(tableView, 0.0); + AnchorPane.setRightAnchor(tableView, 0.0); + tableWrapper.getChildren().setAll(tableView); + + + errorList.setItems(model.getValidationError()); } /** @@ -248,6 +265,17 @@ public class SmartCSVController extends FXMLController { tableView.getColumns().add(column); } + private void scrollToError() { + ValidationError entry = (ValidationError)errorList.getSelectionModel().getSelectedItem(); + if (entry != null) { + if (entry.getLineNumber() != null) { + tableView.scrollTo(entry.getLineNumber()); + tableView.getSelectionModel().select(entry.getLineNumber()); + } else { + tableView.scrollTo(0); + } + } + } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // inner class @@ -277,7 +305,7 @@ public class SmartCSVController extends FXMLController { try { fileReader.read(file); runLater(SmartCSVController.this::resetContent); - } catch (IOException ex) { + } catch (Throwable ex) { ex.printStackTrace(); } } @@ -300,7 +328,7 @@ public class SmartCSVController extends FXMLController { try { csvFileWriter.saveFile(model); runLater(SmartCSVController.this::resetContent); - } catch (IOException ex) { + } catch (Throwable ex) { ex.printStackTrace(); } return null; diff --git a/src/main/java/ninja/javafx/smartcsv/fx/list/ValidationErrorListCell.java b/src/main/java/ninja/javafx/smartcsv/fx/list/ValidationErrorListCell.java new file mode 100644 index 0000000..37a4132 --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/fx/list/ValidationErrorListCell.java @@ -0,0 +1,62 @@ +/* + The MIT License (MIT) + ----------------------------------------------------------------------------- + + Copyright (c) 2015 javafx.ninja + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +package ninja.javafx.smartcsv.fx.list; + +import javafx.scene.control.ListCell; +import javafx.scene.text.Text; +import ninja.javafx.smartcsv.validation.ValidationError; + +/** + * TODO: DESCRIPTION!!! + */ +public class ValidationErrorListCell extends ListCell { + + private Text text; + + @Override + public void updateItem(ValidationError validationError, boolean empty) { + super.updateItem(validationError, empty); + if (empty) { + clearContent(); + } else { + addContent(validationError); + } + } + + + private void clearContent() { + setText(null); + setGraphic(null); + } + + private void addContent(ValidationError validationError) { + setText(null); + text = new Text(validationError.getMessage()); + text.setWrappingWidth(180); + setGraphic(text); + } +} diff --git a/src/main/java/ninja/javafx/smartcsv/fx/table/EditableValidationCell.java b/src/main/java/ninja/javafx/smartcsv/fx/table/EditableValidationCell.java index 9576417..6842527 100644 --- a/src/main/java/ninja/javafx/smartcsv/fx/table/EditableValidationCell.java +++ b/src/main/java/ninja/javafx/smartcsv/fx/table/EditableValidationCell.java @@ -64,12 +64,12 @@ public class EditableValidationCell extends TableCell { protected void updateItem(CSVValue item, boolean empty) { super.updateItem(item, empty); - if (item == null || item.getValid().isValid() || isEditing()) { + if (item == null || item.getValidationError() == null || isEditing()) { setStyle(""); setTooltip(null); - } else if (!item.getValid().isValid()) { + } else if (item.getValidationError() != null) { setStyle("-fx-background-color: #ff8888"); - setTooltip(new Tooltip(item.getValid().error())); + setTooltip(new Tooltip(item.getValidationError().getMessage())); } if (item == null || empty) { diff --git a/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVModel.java b/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVModel.java index 182843f..de2bc28 100644 --- a/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVModel.java +++ b/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVModel.java @@ -28,7 +28,7 @@ package ninja.javafx.smartcsv.fx.table.model; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import ninja.javafx.smartcsv.validation.ValidationState; +import ninja.javafx.smartcsv.validation.ValidationError; import ninja.javafx.smartcsv.validation.Validator; /** @@ -41,6 +41,7 @@ public class CSVModel { private ObservableList rows = FXCollections.observableArrayList(); private String[] header; private String filepath; + private ObservableList validationError = FXCollections.observableArrayList(); public CSVModel(String filepath) { this.filepath = filepath; @@ -71,6 +72,10 @@ public class CSVModel { return rows; } + public ObservableList getValidationError() { + return validationError; + } + /** * adds a new and empty row * @return the new row @@ -105,22 +110,37 @@ public class CSVModel { * walks through the data and validates each value */ private void revalidate() { + validationError.clear(); + if (header != null && validator != null) { - validator.isHeaderValid(header); + addValidationError(validator.isHeaderValid(header)); } - for (CSVRow row: rows) { + for (int lineNumber = 0; lineNumber < rows.size(); lineNumber++) { + CSVRow row = rows.get(lineNumber); row.setValidator(validator); for (String column: row.getColumns().keySet()) { CSVValue value = row.getColumns().get(column).getValue(); value.setValidator(validator); if (validator != null) { - value.setValid(validator.isValid(column, value.getValue())); + ValidationError validationError = validator.isValid(column, value.getValue(), lineNumber); + if (validationError != null) { + addValidationError(validationError); + value.setValidationError(validationError); + } else { + value.setValidationError(null); + } } else { - value.setValid(new ValidationState()); + value.setValidationError(null); } } } } + private void addValidationError(ValidationError validationError) { + if (validationError != null) { + this.validationError.add(validationError); + } + } + } diff --git a/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVValue.java b/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVValue.java index dce22e3..3a573b3 100644 --- a/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVValue.java +++ b/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVValue.java @@ -28,7 +28,7 @@ package ninja.javafx.smartcsv.fx.table.model; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import ninja.javafx.smartcsv.validation.ValidationState; +import ninja.javafx.smartcsv.validation.ValidationError; import ninja.javafx.smartcsv.validation.Validator; /** @@ -41,7 +41,7 @@ public class CSVValue { private int rowNumber; private String column; private StringProperty value = new SimpleStringProperty(); - private ValidationState valid = new ValidationState(); + private ValidationError valid; /** * single value of a cell @@ -89,7 +89,7 @@ public class CSVValue { */ public void setValue(String value) { if (validator != null) { - valid = validator.isValid(column, value); + valid = validator.isValid(column, value, rowNumber); } this.value.set(value); } @@ -98,7 +98,7 @@ public class CSVValue { * returns if the value is valid to the rules of the validator * @return */ - public ValidationState getValid() { + public ValidationError getValidationError() { return valid; } @@ -106,7 +106,7 @@ public class CSVValue { * sets the state if a value is valid or not * @param valid the validation state */ - protected void setValid(ValidationState valid) { + protected void setValidationError(ValidationError valid) { this.valid = valid; } } diff --git a/src/main/java/ninja/javafx/smartcsv/validation/ValidationState.java b/src/main/java/ninja/javafx/smartcsv/validation/ValidationError.java similarity index 76% rename from src/main/java/ninja/javafx/smartcsv/validation/ValidationState.java rename to src/main/java/ninja/javafx/smartcsv/validation/ValidationError.java index b0423ea..c6fbcfe 100644 --- a/src/main/java/ninja/javafx/smartcsv/validation/ValidationState.java +++ b/src/main/java/ninja/javafx/smartcsv/validation/ValidationError.java @@ -31,21 +31,26 @@ import java.io.StringWriter; /** * Created by Andreas on 28.11.2015. */ -public class ValidationState { - private boolean valid = true; - private StringWriter messages = new StringWriter(); +public class ValidationError { - public void invalidate(String message) { - valid = false; - messages.append(message).append('\n'); + private String message; + private Integer lineNumber; + + public ValidationError(String message) { + this(message, -1); } - public boolean isValid() { - return valid; + public ValidationError(String message, Integer lineNumber) { + this.message = message; + this.lineNumber = lineNumber; } - public String error() { - return messages.toString(); + public Integer getLineNumber() { + return lineNumber; + } + + public String getMessage() { + return message; } } diff --git a/src/main/java/ninja/javafx/smartcsv/validation/Validator.java b/src/main/java/ninja/javafx/smartcsv/validation/Validator.java index 324f9d7..80010b7 100644 --- a/src/main/java/ninja/javafx/smartcsv/validation/Validator.java +++ b/src/main/java/ninja/javafx/smartcsv/validation/Validator.java @@ -32,6 +32,7 @@ import groovy.lang.GroovyShell; import groovy.lang.Script; import org.codehaus.groovy.control.CompilationFailedException; +import java.io.StringWriter; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -74,24 +75,30 @@ public class Validator { * checks if the value is valid for the column configuration * @param column the column name * @param value the value to check - * @return ValidationState with information if valid and if not which error happened + * @return ValidationError with information if valid and if not which getMessage happened */ - public ValidationState isValid(String column, String value) { - ValidationState result = new ValidationState(); + public ValidationError isValid(String column, String value, Integer lineNumber) { + ValidationError result = null; if (validationConfig != null) { Config columnSectionConfig = getColumnSectionConfig(); if (columnSectionConfig != null) { Config columnConfig = getColumnConfig(columnSectionConfig, column); if (columnConfig != null) { - checkBlankOrNull(columnConfig, value, result); + + StringWriter errorMessage = new StringWriter(); + checkBlankOrNull(columnConfig, value, errorMessage); if (value != null) { - checkRegularExpression(columnConfig, value, result); - checkAlphaNumeric(columnConfig, value, result); - checkDate(columnConfig, value, result); - checkMaxLength(columnConfig, value, result); - checkMinLength(columnConfig, value, result); - checkInteger(columnConfig, value, result); - checkGroovy(column, columnConfig, value, result); + checkRegularExpression(columnConfig, value, errorMessage); + checkAlphaNumeric(columnConfig, value, errorMessage); + checkDate(columnConfig, value, errorMessage); + checkMaxLength(columnConfig, value, errorMessage); + checkMinLength(columnConfig, value, errorMessage); + checkInteger(columnConfig, value, errorMessage); + checkGroovy(column, columnConfig, value, errorMessage); + } + + if (!errorMessage.toString().isEmpty()) { + result = new ValidationError(errorMessage.toString(), lineNumber); } } } @@ -104,7 +111,7 @@ public class Validator { // private methods //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private void checkGroovy(String column, Config columnConfig, String value, ValidationState result) { + private void checkGroovy(String column, Config columnConfig, String value, StringWriter result) { String groovyScript = getString(columnConfig, "groovy"); if (groovyScript != null) { @@ -122,15 +129,15 @@ public class Validator { try { groovyResult = script.run(); } catch (CompilationFailedException e) { - result.invalidate("groovy script '"+groovyScript+"' throws exception: "+e.getMessage()); + result.append("groovy script '"+groovyScript+"' throws exception: "+e.getMessage()).append('\n'); e.printStackTrace(); } if (groovyResult == null) { - result.invalidate("groovy script '"+groovyScript+"' returns null"); + result.append("groovy script '"+groovyScript+"' returns null").append('\n'); } if (!isScriptResultTrue(groovyResult)) { - result.invalidate(groovyResult.toString()); + result.append(groovyResult.toString()).append('\n'); } } @@ -140,62 +147,62 @@ public class Validator { return groovyResult.equals(true) || groovyResult.toString().trim().toLowerCase().equals("true"); } - private void checkBlankOrNull(Config columnConfig, String value, ValidationState result) { + private void checkBlankOrNull(Config columnConfig, String value, StringWriter result) { if (getBoolean(columnConfig, "not empty")) { if (isBlankOrNull(value)) { - result.invalidate("should not be empty"); + result.append("should not be empty").append('\n'); } } } - private void checkInteger(Config columnConfig, String value, ValidationState result) { + private void checkInteger(Config columnConfig, String value, StringWriter result) { if (getBoolean(columnConfig, "integer")) { if (!isInt(value)) { - result.invalidate("should be an integer"); + result.append("should be an integer").append('\n'); } } } - private void checkMinLength(Config columnConfig, String value, ValidationState result) { + private void checkMinLength(Config columnConfig, String value, StringWriter result) { Integer minLength = getInteger(columnConfig, "minlength"); if (minLength != null) { if (!minLength(value, minLength)) { - result.invalidate("has not min length of " + minLength); + result.append("has not min length of " + minLength).append('\n'); } } } - private void checkMaxLength(Config columnConfig, String value, ValidationState result) { + private void checkMaxLength(Config columnConfig, String value, StringWriter result) { Integer maxLength = getInteger(columnConfig, "maxlength"); if (maxLength != null) { if (!maxLength(value, maxLength)) { - result.invalidate("has not max length of " + maxLength); + result.append("has not max length of " + maxLength).append('\n'); } } } - private void checkDate(Config columnConfig, String value, ValidationState result) { + private void checkDate(Config columnConfig, String value, StringWriter result) { String dateformat = getString(columnConfig, "date"); if (dateformat != null && !dateformat.trim().isEmpty()) { if (!isDate(value, dateformat, true)) { - result.invalidate("is not a date of format " + dateformat); + result.append("is not a date of format " + dateformat).append('\n'); } } } - private void checkAlphaNumeric(Config columnConfig, String value, ValidationState result) { + private void checkAlphaNumeric(Config columnConfig, String value, StringWriter result) { if (getBoolean(columnConfig, "alphanumeric")) { if (!matchRegexp(value, "[0-9a-zA-Z]*")) { - result.invalidate("should not be alphanumeric"); + result.append("should not be alphanumeric").append('\n'); } } } - private void checkRegularExpression(Config columnConfig, String value, ValidationState result) { + private void checkRegularExpression(Config columnConfig, String value, StringWriter result) { String regexp = getString(columnConfig, "regexp"); if (regexp != null && !regexp.trim().isEmpty()) { if (!matchRegexp(value, regexp)) { - result.invalidate("does not match " + regexp); + result.append("does not match " + regexp).append('\n'); } } } @@ -222,8 +229,8 @@ public class Validator { return columnConfig.hasPath(path) && columnConfig.getBoolean(path); } - public ValidationState isHeaderValid(String[] headerNames) { - ValidationState result = new ValidationState(); + public ValidationError isHeaderValid(String[] headerNames) { + ValidationError result = null; if (validationConfig != null) { if (validationConfig.hasPath("headers")) { Config headerSectionConfig = validationConfig.getConfig("headers"); @@ -231,25 +238,30 @@ public class Validator { List headerConfig = headerSectionConfig.getStringList("list"); if (headerConfig != null) { if (headerNames.length != headerConfig.size()) { - result.invalidate("number of headers is not correct! there are " + + result = new ValidationError("number of headers is not correct! there are " + headerNames.length + " but there should be " + - headerConfig.size()); + headerConfig.size()+ "\n"); return result; } + StringWriter errorMessage = new StringWriter(); + for(int i=0; i
- - - - + + + + + + + + + + + + + + + +
@@ -54,4 +70,6 @@ + + diff --git a/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVModelTest.java b/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVModelTest.java index 8ff6939..67fb072 100644 --- a/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVModelTest.java +++ b/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVModelTest.java @@ -88,7 +88,7 @@ public class CSVModelTest { sut.setValidator(validator); // assertion - verify(validator).isValid(TESTHEADER, TESTVALUE); + verify(validator).isValid(TESTHEADER, TESTVALUE, 0); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVValueTest.java b/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVValueTest.java index b2bff24..b5148f2 100644 --- a/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVValueTest.java +++ b/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVValueTest.java @@ -4,6 +4,7 @@ import ninja.javafx.smartcsv.validation.Validator; import org.junit.Before; import org.junit.Test; +import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -53,6 +54,6 @@ public class CSVValueTest { sut.setValue(VALUE); // assertion - verify(validator).isValid(COLUMN, VALUE); + verify(validator).isValid(COLUMN, VALUE, anyInt()); } } \ No newline at end of file diff --git a/src/test/java/ninja/javafx/smartcsv/validation/HeaderValidationTest.java b/src/test/java/ninja/javafx/smartcsv/validation/HeaderValidationTest.java index cf6d393..22bdad7 100644 --- a/src/test/java/ninja/javafx/smartcsv/validation/HeaderValidationTest.java +++ b/src/test/java/ninja/javafx/smartcsv/validation/HeaderValidationTest.java @@ -86,12 +86,12 @@ public class HeaderValidationTest { @Test public void validation() { // execution - ValidationState result = sut.isHeaderValid(headerNames); + ValidationError result = sut.isHeaderValid(headerNames); // assertion - assertThat(result.isValid(), is(expectedResult)); + assertThat(result == null, is(expectedResult)); if (!expectedResult) { - assertThat(result.error(), is(expectedError)); + assertThat(result.getMessage(), is(expectedError)); } } diff --git a/src/test/java/ninja/javafx/smartcsv/validation/ValidatorTest.java b/src/test/java/ninja/javafx/smartcsv/validation/ValidatorTest.java index 4682158..48faa7b 100644 --- a/src/test/java/ninja/javafx/smartcsv/validation/ValidatorTest.java +++ b/src/test/java/ninja/javafx/smartcsv/validation/ValidatorTest.java @@ -68,12 +68,12 @@ public class ValidatorTest { @Test public void validation() { // execution - ValidationState result = sut.isValid(column, value); + ValidationError result = sut.isValid(column, value, 0); // assertion - assertThat(result.isValid(), is(expectedResult)); + assertThat(result == null, is(expectedResult)); if (!expectedResult) { - assertThat(result.error(), is(expectedError)); + assertThat(result.getMessage(), is(expectedError)); } }