From 07084bd18e77a90affcbfcd10e22364ce5d7a068 Mon Sep 17 00:00:00 2001 From: Andreas Billmann Date: Sat, 28 Nov 2015 23:06:14 +0100 Subject: [PATCH] Initial commit --- build.gradle | 27 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 51010 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 ++++++++++ gradlew.bat | 90 ++++++ license | 28 ++ settings.gradle | 2 + .../ninja/javafx/smartcsv/FileReader.java | 37 +++ .../javafx/smartcsv/csv/CSVFileReader.java | 80 +++++ .../javafx/smartcsv/csv/CSVFileWriter.java | 80 +++++ .../javafx/smartcsv/fx/FXMLController.java | 72 +++++ .../ninja/javafx/smartcsv/fx/SmartCSV.java | 84 +++++ .../smartcsv/fx/SmartCSVController.java | 290 ++++++++++++++++++ .../fx/table/EditableValidationCell.java | 136 ++++++++ .../fx/table/ObservableMapValueFactory.java | 53 ++++ .../fx/table/ValidationCellFactory.java | 45 +++ .../smartcsv/fx/table/model/CSVModel.java | 121 ++++++++ .../smartcsv/fx/table/model/CSVRow.java | 90 ++++++ .../smartcsv/fx/table/model/CSVValue.java | 112 +++++++ .../validation/ValidationFileReader.java | 53 ++++ .../smartcsv/validation/ValidationState.java | 51 +++ .../javafx/smartcsv/validation/Validator.java | 216 +++++++++++++ .../javafx/smartcsv/fx/application.properties | 8 + .../ninja/javafx/smartcsv/fx/smartcsv.css | 0 .../ninja/javafx/smartcsv/fx/smartcsv.fxml | 57 ++++ .../javafx/smartcsv/fx/smartcsv_de.properties | 52 ++++ .../javafx/smartcsv/fx/smartcsv_en.properties | 53 ++++ .../smartcsv/fx/table/model/CSVModelTest.java | 103 +++++++ .../smartcsv/fx/table/model/CSVRowTest.java | 43 +++ .../smartcsv/fx/table/model/CSVValueTest.java | 58 ++++ .../smartcsv/validation/ValidatorTest.java | 131 ++++++++ 31 files changed, 2342 insertions(+) create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 license create mode 100644 settings.gradle create mode 100644 src/main/java/ninja/javafx/smartcsv/FileReader.java create mode 100644 src/main/java/ninja/javafx/smartcsv/csv/CSVFileReader.java create mode 100644 src/main/java/ninja/javafx/smartcsv/csv/CSVFileWriter.java create mode 100644 src/main/java/ninja/javafx/smartcsv/fx/FXMLController.java create mode 100644 src/main/java/ninja/javafx/smartcsv/fx/SmartCSV.java create mode 100644 src/main/java/ninja/javafx/smartcsv/fx/SmartCSVController.java create mode 100644 src/main/java/ninja/javafx/smartcsv/fx/table/EditableValidationCell.java create mode 100644 src/main/java/ninja/javafx/smartcsv/fx/table/ObservableMapValueFactory.java create mode 100644 src/main/java/ninja/javafx/smartcsv/fx/table/ValidationCellFactory.java create mode 100644 src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVModel.java create mode 100644 src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVRow.java create mode 100644 src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVValue.java create mode 100644 src/main/java/ninja/javafx/smartcsv/validation/ValidationFileReader.java create mode 100644 src/main/java/ninja/javafx/smartcsv/validation/ValidationState.java create mode 100644 src/main/java/ninja/javafx/smartcsv/validation/Validator.java create mode 100644 src/main/resources/ninja/javafx/smartcsv/fx/application.properties create mode 100644 src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.css create mode 100644 src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.fxml create mode 100644 src/main/resources/ninja/javafx/smartcsv/fx/smartcsv_de.properties create mode 100644 src/main/resources/ninja/javafx/smartcsv/fx/smartcsv_en.properties create mode 100644 src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVModelTest.java create mode 100644 src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVRowTest.java create mode 100644 src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVValueTest.java create mode 100644 src/test/java/ninja/javafx/smartcsv/validation/ValidatorTest.java diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..4d4ef67 --- /dev/null +++ b/build.gradle @@ -0,0 +1,27 @@ +group 'ninja.javafx' +version '1.0-SNAPSHOT' + +apply plugin: 'java' +apply plugin: 'groovy' +apply plugin:'application' +apply plugin: 'idea' + +sourceCompatibility = 1.8 +mainClassName = 'ninja.javafx.smartcsv.fx.SmartCSV' + +repositories { + mavenCentral() + jcenter() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' + testCompile group: 'org.testfx', name: 'testfx-core', version: '4.0.+' + testCompile group: 'org.testfx', name: 'testfx-junit', version: '4.0.+' + 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' + compile group: 'net.sf.supercsv', name: 'super-csv', version: '2.4.0' + compile group: 'com.typesafe', name: 'config', version: '1.3.0' + compile group: 'commons-validator', name: 'commons-validator', version: '1.4.1' +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2322723c7ed5f591adfa4c87b6c51932f591249a GIT binary patch literal 51010 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∓LFg$ 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..018cbda --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Oct 01 19:02:27 CEST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/license b/license new file mode 100644 index 0000000..a57e612 --- /dev/null +++ b/license @@ -0,0 +1,28 @@ +The MIT License (MIT) +------------------------------------------------------------------------------------------------------------------------ + +Copyright (c) 2015 Andreas Billmann + + + +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. \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..0d0cf9a --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'SmartCSV.ninja.javafx.smartcsv.fx' + diff --git a/src/main/java/ninja/javafx/smartcsv/FileReader.java b/src/main/java/ninja/javafx/smartcsv/FileReader.java new file mode 100644 index 0000000..43ab82a --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/FileReader.java @@ -0,0 +1,37 @@ +/* + 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; + +import java.io.File; +import java.io.IOException; + +/** + * Created by Andreas on 19.11.2015. + */ +public interface FileReader { + void read(File filename) throws IOException; +} diff --git a/src/main/java/ninja/javafx/smartcsv/csv/CSVFileReader.java b/src/main/java/ninja/javafx/smartcsv/csv/CSVFileReader.java new file mode 100644 index 0000000..c4dbb0d --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/csv/CSVFileReader.java @@ -0,0 +1,80 @@ +/* + 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.csv; + +import ninja.javafx.smartcsv.FileReader; +import ninja.javafx.smartcsv.fx.table.model.CSVModel; +import ninja.javafx.smartcsv.fx.table.model.CSVRow; +import org.springframework.stereotype.Service; +import org.supercsv.io.CsvMapReader; +import org.supercsv.io.ICsvMapReader; +import org.supercsv.prefs.CsvPreference; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +/** + * Created by Andreas on 18.11.2015. + */ +@Service +public class CSVFileReader implements FileReader { + + private CSVModel model; + + @Override + public void read(File file) throws IOException { + + ICsvMapReader mapReader = null; + try { + mapReader = new CsvMapReader(new java.io.FileReader(file.getAbsoluteFile()), CsvPreference.EXCEL_NORTH_EUROPE_PREFERENCE); + model = new CSVModel(file.getAbsolutePath()); + + // the header columns are used as the keys to the Map + String[] header = mapReader.getHeader(true); + model.setHeader(header); + + Map customerMap; + while ((customerMap = mapReader.read(header)) != null) { + CSVRow row = model.addRow(); + for (String column : header) { + row.addValue(column, customerMap.get(column)); + } + } + + } finally { + if (mapReader != null) { + mapReader.close(); + } + } + } + + public CSVModel getData() { + return model; + } + +} diff --git a/src/main/java/ninja/javafx/smartcsv/csv/CSVFileWriter.java b/src/main/java/ninja/javafx/smartcsv/csv/CSVFileWriter.java new file mode 100644 index 0000000..7b6e097 --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/csv/CSVFileWriter.java @@ -0,0 +1,80 @@ +/* + 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.csv; + +import ninja.javafx.smartcsv.fx.table.model.CSVModel; +import ninja.javafx.smartcsv.fx.table.model.CSVRow; +import org.springframework.stereotype.Service; +import org.supercsv.io.CsvMapWriter; +import org.supercsv.io.ICsvMapWriter; +import org.supercsv.prefs.CsvPreference; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.Map; + +import static java.util.stream.Collectors.toMap; + +/** + * filewriter for the csv + */ +@Service +public class CSVFileWriter { + + public void saveFile(CSVModel model) throws IOException { + ICsvMapWriter mapWriter = null; + try { + mapWriter = new CsvMapWriter(new FileWriter(model.getFilepath()), CsvPreference.EXCEL_NORTH_EUROPE_PREFERENCE); + mapWriter.writeHeader(model.getHeader()); + + for(CSVRow row: model.getRows()) { + Map columns = convertMapFromModel(row); + mapWriter.write(columns, model.getHeader()); + } + } + finally { + if( mapWriter != null ) { + mapWriter.close(); + } + } + } + + /** + * transforms the column map from CSVValue to a simple Map + * @param row the row to convert + * @return a simple map for the supercvs writer + */ + private Map convertMapFromModel(CSVRow row) { + return row.getColumns().entrySet().stream() + .collect( + toMap( + Map.Entry::getKey, + e -> e.getValue().getValue().getValue() != null ? e.getValue().getValue().getValue() : "" + ) + ); + } +} diff --git a/src/main/java/ninja/javafx/smartcsv/fx/FXMLController.java b/src/main/java/ninja/javafx/smartcsv/fx/FXMLController.java new file mode 100644 index 0000000..cad8fb9 --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/fx/FXMLController.java @@ -0,0 +1,72 @@ +/* + 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; + + +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ResourceBundle; + +public abstract class FXMLController implements InitializingBean, Initializable { + + protected Node view; + protected String fxmlFilePath; + protected String resourcePath; + + public abstract void setFxmlFilePath(String filePath); + + @Value("${resource.main}") + public void setResourceBundle(String resourcePath) { + this.resourcePath = resourcePath; + } + + @Override + public void afterPropertiesSet() throws Exception { + loadFXML(); + } + + protected final void loadFXML() throws IOException { + try (InputStream fxmlStream = getClass().getResourceAsStream(fxmlFilePath)) { + FXMLLoader loader = new FXMLLoader(); + loader.setResources(ResourceBundle.getBundle(this.resourcePath)); + loader.setController(this); + this.view = (loader.load(fxmlStream)); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + public Node getView() { + return view; + } +} \ No newline at end of file diff --git a/src/main/java/ninja/javafx/smartcsv/fx/SmartCSV.java b/src/main/java/ninja/javafx/smartcsv/fx/SmartCSV.java new file mode 100644 index 0000000..2907e20 --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/fx/SmartCSV.java @@ -0,0 +1,84 @@ +/* + 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; + +import javafx.application.Application; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +import org.springframework.context.annotation.*; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; + +@Configuration +@ComponentScan("ninja.javafx") +@PropertySource(value = "classpath:/ninja/javafx/smartcsv/fx/application.properties") +public class SmartCSV extends Application { + + private AnnotationConfigApplicationContext appContext; + + @Override + public void start(Stage primaryStage) throws Exception { + appContext = new AnnotationConfigApplicationContext(SmartCSV.class); + String name = appContext.getEnvironment().getProperty("application.name"); + String version = appContext.getEnvironment().getProperty("application.version"); + + try { + showUI(primaryStage, name, version); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + @Bean + public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Override + public void stop() throws Exception { + if (appContext != null) { + appContext.close(); + } + + super.stop(); + } + + public static void main(String[] args) { + launch(args); + } + + private void showUI(Stage primaryStage, String name, String version) { + SmartCSVController smartCVSController = appContext.getBean(SmartCSVController.class); + Scene scene = new Scene((Parent) smartCVSController.getView()); + + primaryStage.setScene(scene); + primaryStage.setTitle(String.format("%s %s", name, version)); + primaryStage.show(); + } + +} diff --git a/src/main/java/ninja/javafx/smartcsv/fx/SmartCSVController.java b/src/main/java/ninja/javafx/smartcsv/fx/SmartCSVController.java new file mode 100644 index 0000000..2f8c74c --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/fx/SmartCSVController.java @@ -0,0 +1,290 @@ +/* + 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; + +import javafx.application.Platform; +import javafx.concurrent.Service; +import javafx.concurrent.Task; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +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.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.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; + +import static javafx.application.Platform.runLater; + +/** + * main controller of the application + */ +@Component +public class SmartCSVController extends FXMLController { + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // injections + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Autowired + private CSVFileReader csvLoader; + + @Autowired + private ValidationFileReader validationLoader; + + @Autowired + private CSVFileWriter csvFileWriter; + + @FXML + private BorderPane applicationPane; + + @FXML + private Label stateline; + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // injections + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private ValidationCellFactory cellFactory = new ValidationCellFactory(); + private final LoadCSVService loadCSVService = new LoadCSVService(); + private final SaveCSVService saveCSVService = new SaveCSVService(); + private CSVModel model; + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // init + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void initialize(URL location, ResourceBundle resources) { + stateline.setVisible(false); + } + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // setter + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Value("${fxml.smartcvs.view}") + @Override + public void setFxmlFilePath(String filePath) { + this.fxmlFilePath = filePath; + } + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // actions + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @FXML + public void openCsv(ActionEvent actionEvent) { + loadFile(csvLoader, "CSV files (*.csv)", "*.csv", "Open CSV"); + } + + @FXML + public void openConfig(ActionEvent actionEvent) { + loadFile(validationLoader, "JSON files (*.json)", "*.json", "Open Validation Configuration"); + } + + @FXML + public void saveCsv(ActionEvent actionEvent) { + saveCSVService.restart(); + } + + @FXML + public void saveAsCsv(ActionEvent actionEvent) { + saveFile(csvFileWriter, "CSV files (*.csv)", "*.csv"); + } + + @FXML + public void close(ActionEvent actionEvent) { + Platform.exit(); + } + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // private methods + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void loadFile(FileReader fileReader, String filterText, String filter, String title) { + final FileChooser fileChooser = new FileChooser(); + + //Set extension filter + final FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter(filterText, filter); + fileChooser.getExtensionFilters().add(extFilter); + fileChooser.setTitle(title); + + //Show open file dialog + final File file = fileChooser.showOpenDialog(applicationPane.getScene().getWindow()); + if (file != null) { + loadCSVService.setFile(file); + loadCSVService.setFileReader(fileReader); + loadCSVService.restart(); + } + } + + private void saveFile(CSVFileWriter writer, String filterText, String filter) { + if (model != null) { + final FileChooser fileChooser = new FileChooser(); + + //Set extension filter + final FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter(filterText, filter); + fileChooser.getExtensionFilters().add(extFilter); + + File initfile = new File(model.getFilepath()); + fileChooser.setInitialDirectory(initfile.getParentFile()); + fileChooser.setInitialFileName(initfile.getName()); + fileChooser.setTitle("Save File"); + + //Show open file dialog + final File file = fileChooser.showOpenDialog(applicationPane.getScene().getWindow()); + if (file != null) { + model.setFilepath(file.getAbsolutePath()); + saveCSVService.restart(); + } + } + } + + /** + * Creates new table view and add the new content + */ + private void resetContent() { + model = csvLoader.getData(); + model.setValidator(validationLoader.getValidator()); + + TableView tableView = new TableView<>(); + + for (String column: model.getHeader()) { + addColumn(column, tableView); + } + tableView.getItems().setAll(model.getRows()); + tableView.setEditable(true); + + applicationPane.setCenter(tableView); + } + + /** + * Adds a column with the given name to the tableview + * @param header name of the column header + * @param tableView the tableview + */ + private void addColumn(String header, TableView tableView) { + TableColumn column = new TableColumn(header); + column.setCellValueFactory(new ObservableMapValueFactory(header)); + column.setCellFactory(cellFactory); + column.setEditable(true); + column.setOnEditCommit(new EventHandler>() { + @Override + public void handle(TableColumn.CellEditEvent event) { + event.getTableView().getItems().get(event.getTablePosition().getRow()). + getColumns().get(header). + setValue(event.getNewValue()); + } + }); + + tableView.getColumns().add(column); + } + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // inner class + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Service class for async load of a csv file + */ + private class LoadCSVService extends Service { + + private File file = null; + private FileReader fileReader; + + public void setFile(File value) { + file = value; + } + public void setFileReader(FileReader fileReader) { + this.fileReader = fileReader; + } + + @Override + protected Task createTask() { + return new Task() { + @Override + protected Void call() throws Exception { + if (file != null) { + try { + fileReader.read(file); + runLater(SmartCSVController.this::resetContent); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + return null; + } + }; + } + } + + /** + * Service class for async load of a csv file + */ + private class SaveCSVService extends Service { + + @Override + protected Task createTask() { + return new Task() { + @Override + protected Void call() throws Exception { + try { + csvFileWriter.saveFile(model); + runLater(SmartCSVController.this::resetContent); + } catch (IOException ex) { + ex.printStackTrace(); + } + return null; + } + }; + } + } +} diff --git a/src/main/java/ninja/javafx/smartcsv/fx/table/EditableValidationCell.java b/src/main/java/ninja/javafx/smartcsv/fx/table/EditableValidationCell.java new file mode 100644 index 0000000..9576417 --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/fx/table/EditableValidationCell.java @@ -0,0 +1,136 @@ +/* + 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.table; + +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.TableCell; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.input.KeyCode; +import ninja.javafx.smartcsv.fx.table.model.CSVRow; +import ninja.javafx.smartcsv.fx.table.model.CSVValue; + +import static javafx.application.Platform.runLater; + +/** + * Created by Andreas on 27.11.2015. + */ +public class EditableValidationCell extends TableCell { + + private ValueTextField textField; + + @Override + public void startEdit() { + super.startEdit(); + setTextField(); + runLater(() -> { + textField.requestFocus(); + textField.selectAll(); + }); + } + + @Override + public void cancelEdit() { + super.cancelEdit(); + setText(getItem().getValue()); + setContentDisplay(ContentDisplay.TEXT_ONLY); + } + + @Override + protected void updateItem(CSVValue item, boolean empty) { + super.updateItem(item, empty); + + if (item == null || item.getValid().isValid() || isEditing()) { + setStyle(""); + setTooltip(null); + } else if (!item.getValid().isValid()) { + setStyle("-fx-background-color: #ff8888"); + setTooltip(new Tooltip(item.getValid().error())); + } + + if (item == null || empty) { + setTextInCell(null); + } else { + if (isEditing()) { + setTextField(); + textField.setValue(item); + } else { + setTextInCell(item.getValue()); + } + } + } + + private void setTextField() { + if (textField == null) { + createTextField(); + } + setGraphic(textField); + setContentDisplay(ContentDisplay.GRAPHIC_ONLY); + } + + private void setTextInCell(String text) { + setGraphic(null); + setText(text); + setContentDisplay(ContentDisplay.TEXT_ONLY); + } + + private void createTextField() { + textField = new ValueTextField(getItem()); + textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2); + textField.setOnKeyPressed(t -> { + if (t.getCode() == KeyCode.ENTER) { + commitEdit(textField.getValue()); + } else if (t.getCode() == KeyCode.ESCAPE) { + cancelEdit(); + } + }); + textField.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue && textField != null) { + commitEdit(textField.getValue()); + } + }); + } + + private class ValueTextField extends TextField { + private CSVValue value; + + public ValueTextField(CSVValue value) { + setValue(value); + } + + public void setValue(CSVValue value) { + this.value = value; + setText(value.getValue()); + } + + public CSVValue getValue() { + value.setValue(getText()); + return value; + } + + } +} diff --git a/src/main/java/ninja/javafx/smartcsv/fx/table/ObservableMapValueFactory.java b/src/main/java/ninja/javafx/smartcsv/fx/table/ObservableMapValueFactory.java new file mode 100644 index 0000000..2bacdba --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/fx/table/ObservableMapValueFactory.java @@ -0,0 +1,53 @@ +/* + 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.table; + +import javafx.beans.property.ObjectProperty; +import javafx.scene.control.TableColumn; +import javafx.util.Callback; +import ninja.javafx.smartcsv.fx.table.model.CSVRow; +import ninja.javafx.smartcsv.fx.table.model.CSVValue; + +/** + * Created by Andreas on 18.11.2015. + */ +public class ObservableMapValueFactory implements + Callback, ObjectProperty> { + + private final Object key; + + public ObservableMapValueFactory(Object key) { + this.key = key; + } + + @Override + public ObjectProperty call(TableColumn.CellDataFeatures features) { + CSVRow row = features.getValue(); + ObjectProperty value = row.getColumns().get(key); + return value; + } +} diff --git a/src/main/java/ninja/javafx/smartcsv/fx/table/ValidationCellFactory.java b/src/main/java/ninja/javafx/smartcsv/fx/table/ValidationCellFactory.java new file mode 100644 index 0000000..3e77d69 --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/fx/table/ValidationCellFactory.java @@ -0,0 +1,45 @@ +/* + 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.table; + +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.util.Callback; +import ninja.javafx.smartcsv.fx.table.model.CSVModel; +import ninja.javafx.smartcsv.fx.table.model.CSVRow; +import ninja.javafx.smartcsv.fx.table.model.CSVValue; + +/** + * Created by Andreas on 18.11.2015. + */ +public class ValidationCellFactory implements Callback, TableCell> { + + @Override + public TableCell call(TableColumn param) { + return new EditableValidationCell(); + } +} 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 new file mode 100644 index 0000000..7a2f31e --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVModel.java @@ -0,0 +1,121 @@ +/* + 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.table.model; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import ninja.javafx.smartcsv.validation.ValidationState; +import ninja.javafx.smartcsv.validation.Validator; + +/** + * The CSVModel is the client representation for the csv filepath. + * It holds the data in rows, stores the header and manages the validator. + */ +public class CSVModel { + + private Validator validator; + private ObservableList rows = FXCollections.observableArrayList(); + private String[] header; + private String filepath; + + public CSVModel(String filepath) { + this.filepath = filepath; + } + + public String getFilepath() { + return this.filepath; + } + + public void setFilepath(String filepath) { + this.filepath = filepath; + } + + /** + * sets the validator for the data revalidates + * @param validator the validator for this data + */ + public void setValidator(Validator validator) { + this.validator = validator; + revalidate(); + } + + /** + * returns the data as a list of rows of the + * @return list of rows + */ + public ObservableList getRows() { + return rows; + } + + /** + * adds a new and empty row + * @return the new row + */ + public CSVRow addRow() { + CSVRow row = new CSVRow(); + row.setValidator(validator); + row.setRowNumber(rows.size()); + rows.add(row); + return row; + } + + /** + * sets the column headers as string array + * @param header the headers of the columns + */ + public void setHeader(String[] header) { + this.header = header; + } + + /** + * returns the column headers + * @return the column headers + */ + public String[] getHeader() { + return header; + } + + + /** + * walks through the data and validates each value + */ + private void revalidate() { + for (CSVRow row: rows) { + 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())); + } else { + value.setValid(new ValidationState()); + } + } + } + } + +} diff --git a/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVRow.java b/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVRow.java new file mode 100644 index 0000000..ac2bffc --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVRow.java @@ -0,0 +1,90 @@ +/* + 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.table.model; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableMap; +import ninja.javafx.smartcsv.validation.Validator; + +/** + * This class represents a single row in the csv file. + */ +public class CSVRow { + private Validator validator; + private ObservableMap> columns = FXCollections.observableHashMap(); + private int rowNumber; + + /** + * single row + * @param validator the reference to the validator + */ + public void setValidator(Validator validator) { + this.validator = validator; + } + + /** + * sets the row number + * @param rowNumber + */ + public void setRowNumber(int rowNumber) { + this.rowNumber = rowNumber; + } + + /** + * return the row number + * @return row number + */ + public int getRowNumber() { + return rowNumber; + } + + + /** + * returns the columns with data as Map + * @return columns with data + */ + public ObservableMap> getColumns() { + return columns; + } + + /** + * stores the given value in the given column of this row + * @param column column name + * @param value the value to store + */ + public void addValue(String column, String value) { + CSVValue v = new CSVValue(); + v.setValidator(validator); + v.setValue(value); + v.setColumn(column); + v.setRowNumber(rowNumber); + columns.put(column, new SimpleObjectProperty<>(v)); + } + +} 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 new file mode 100644 index 0000000..dce22e3 --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/fx/table/model/CSVValue.java @@ -0,0 +1,112 @@ +/* + 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.table.model; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import ninja.javafx.smartcsv.validation.ValidationState; +import ninja.javafx.smartcsv.validation.Validator; + +/** + * The csv value represents the value of a single cell. + * It also knows about the position (row and column) + * and if the value is valid based on the validator. + */ +public class CSVValue { + private Validator validator; + private int rowNumber; + private String column; + private StringProperty value = new SimpleStringProperty(); + private ValidationState valid = new ValidationState(); + + /** + * single value of a cell + * @param validator the reference to the validator + */ + public void setValidator(Validator validator) { + this.validator = validator; + } + + /** + * the row number this value is stored in + * @param row row number + */ + public void setRowNumber(int row) { + this.rowNumber = row; + } + + /** + * the column this value is stored in + * @param column header name of the column + */ + public void setColumn(String column) { + this.column = column; + } + + /** + * returns the real value + * @return the real value + */ + public String getValue() { + return value.get(); + } + + /** + * JavaFX property representation of the real value + * @return property of real value + */ + public StringProperty valueProperty() { + return value; + } + + /** + * sets the real value + * @param value the real value + */ + public void setValue(String value) { + if (validator != null) { + valid = validator.isValid(column, value); + } + this.value.set(value); + } + + /** + * returns if the value is valid to the rules of the validator + * @return + */ + public ValidationState getValid() { + return valid; + } + + /** + * sets the state if a value is valid or not + * @param valid the validation state + */ + protected void setValid(ValidationState valid) { + this.valid = valid; + } +} diff --git a/src/main/java/ninja/javafx/smartcsv/validation/ValidationFileReader.java b/src/main/java/ninja/javafx/smartcsv/validation/ValidationFileReader.java new file mode 100644 index 0000000..82a29a9 --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/validation/ValidationFileReader.java @@ -0,0 +1,53 @@ +/* + 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.validation; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import ninja.javafx.smartcsv.FileReader; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.IOException; + +/** + * This class loads the constraints as json config + */ +@Service +public class ValidationFileReader implements FileReader { + + private Config config; + + @Override + public void read(File file) throws IOException { + config = ConfigFactory.parseFile(file); + } + + public Validator getValidator() { + return new Validator(config); + } +} diff --git a/src/main/java/ninja/javafx/smartcsv/validation/ValidationState.java b/src/main/java/ninja/javafx/smartcsv/validation/ValidationState.java new file mode 100644 index 0000000..b0423ea --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/validation/ValidationState.java @@ -0,0 +1,51 @@ +/* + 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.validation; + +import java.io.StringWriter; + +/** + * Created by Andreas on 28.11.2015. + */ +public class ValidationState { + private boolean valid = true; + private StringWriter messages = new StringWriter(); + + public void invalidate(String message) { + valid = false; + messages.append(message).append('\n'); + } + + public boolean isValid() { + return valid; + } + + public String error() { + return messages.toString(); + } + +} diff --git a/src/main/java/ninja/javafx/smartcsv/validation/Validator.java b/src/main/java/ninja/javafx/smartcsv/validation/Validator.java new file mode 100644 index 0000000..5b5614e --- /dev/null +++ b/src/main/java/ninja/javafx/smartcsv/validation/Validator.java @@ -0,0 +1,216 @@ +/* + 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.validation; + +import com.typesafe.config.Config; +import groovy.lang.Binding; +import groovy.lang.GroovyShell; +import groovy.lang.Script; +import org.codehaus.groovy.control.CompilationFailedException; + +import java.util.HashMap; +import java.util.Map; + +import static org.apache.commons.validator.GenericValidator.*; + +/** + * This class checks all the validations defined in the + * Config against a given value + */ +public class Validator { + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // member variables + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private Config validationConfig; + private GroovyShell shell = new GroovyShell(); + private Map scriptCache = new HashMap<>(); + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // constructors + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * JSON configuration for this validator + * @param validationConfig + */ + public Validator(Config validationConfig) { + this.validationConfig = validationConfig; + } + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // oublic methods + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * 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 + */ + public ValidationState isValid(String column, String value) { + ValidationState result = new ValidationState(); + if (validationConfig != null) { + Config columnConfig = getColumnConfig(column); + if (columnConfig != null) { + checkBlankOrNull(columnConfig, value, result); + 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); + } + } + } + return result; + } + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // private methods + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void checkGroovy(String column, Config columnConfig, String value, ValidationState result) { + String groovyScript = getString(columnConfig, "groovy"); + if (groovyScript != null) { + + Script script = scriptCache.get(column); + if (script == null) { + script = shell.parse(groovyScript); + scriptCache.put(column, script); + } + + Binding binding = new Binding(); + binding.setVariable("value", value); + script.setBinding(binding); + + Object groovyResult = null; + try { + groovyResult = script.run(); + } catch (CompilationFailedException e) { + result.invalidate("groovy script '"+groovyScript+"' throws exception: "+e.getMessage()); + e.printStackTrace(); + } + if (groovyResult == null) { + result.invalidate("groovy script '"+groovyScript+"' returns null"); + } + + if (!isScriptResultTrue(groovyResult)) { + result.invalidate(groovyResult.toString()); + } + + } + } + + private boolean isScriptResultTrue(Object groovyResult) { + return groovyResult.equals(true) || groovyResult.toString().trim().toLowerCase().equals("true"); + } + + private void checkBlankOrNull(Config columnConfig, String value, ValidationState result) { + if (getBoolean(columnConfig, "not empty")) { + if (isBlankOrNull(value)) { + result.invalidate("should not be empty"); + } + } + } + + private void checkInteger(Config columnConfig, String value, ValidationState result) { + if (getBoolean(columnConfig, "integer")) { + if (!isInt(value)) { + result.invalidate("should be an integer"); + } + } + } + + private void checkMinLength(Config columnConfig, String value, ValidationState result) { + Integer minLength = getInteger(columnConfig, "minlength"); + if (minLength != null) { + if (!minLength(value, minLength)) { + result.invalidate("has not min length of " + minLength); + } + } + } + + private void checkMaxLength(Config columnConfig, String value, ValidationState result) { + Integer maxLength = getInteger(columnConfig, "maxlength"); + if (maxLength != null) { + if (!maxLength(value, maxLength)) { + result.invalidate("has not max length of " + maxLength); + } + } + } + + private void checkDate(Config columnConfig, String value, ValidationState 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); + } + } + } + + private void checkAlphaNumeric(Config columnConfig, String value, ValidationState result) { + if (getBoolean(columnConfig, "alphanumeric")) { + if (!matchRegexp(value, "[0-9a-zA-Z]*")) { + result.invalidate("should not be alphanumeric"); + } + } + } + + private void checkRegularExpression(Config columnConfig, String value, ValidationState result) { + String regexp = getString(columnConfig, "regexp"); + if (regexp != null && !regexp.trim().isEmpty()) { + if (!matchRegexp(value, regexp)) { + result.invalidate("does not match " + regexp); + } + } + } + + private Config getColumnConfig(String column) { + return validationConfig.hasPath(column) ? validationConfig.getConfig(column) : null; + } + + private String getString(Config columnConfig, String path) { + return columnConfig.hasPath(path) ? columnConfig.getString(path) : null; + } + + private Integer getInteger(Config columnConfig, String path) { + return columnConfig.hasPath(path) ? columnConfig.getInt(path) : null; + } + + + private boolean getBoolean(Config columnConfig, String path) { + return columnConfig.hasPath(path) && columnConfig.getBoolean(path); + } + +} diff --git a/src/main/resources/ninja/javafx/smartcsv/fx/application.properties b/src/main/resources/ninja/javafx/smartcsv/fx/application.properties new file mode 100644 index 0000000..dce16ec --- /dev/null +++ b/src/main/resources/ninja/javafx/smartcsv/fx/application.properties @@ -0,0 +1,8 @@ +application.name = SmartCSV.fx +application.version = 0.1 + +# fxml views +fxml.smartcvs.view = /ninja/javafx/smartcsv/fx/smartcsv.fxml + +# resources +resource.main = ninja.javafx.smartcsv.fx.smartcsv \ No newline at end of file diff --git a/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.css b/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.css new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.fxml b/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.fxml new file mode 100644 index 0000000..50ff2c9 --- /dev/null +++ b/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv.fxml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + + +
diff --git a/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv_de.properties b/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv_de.properties new file mode 100644 index 0000000..abd2741 --- /dev/null +++ b/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv_de.properties @@ -0,0 +1,52 @@ +menu.title.import.gpx = Importiere GPX +menu.title.file = Datei +menu.title.quit = Beenden +menu.title.help = Hilfe +menu.title.settings = Einstellungen +menu.title.about = \u00dcber GeoFroggerFX +menu.title.plugins = Plugins +menu.title.list = Listen +menu.title.list.new = Liste anlegen +menu.title.list.delete = Liste l\u00F6schen + +menu.title.sort = Sortieren +menu.title.filter = Filtern + +label.text.cache.list=Liste: +label.text.cache.list.count=Anzahl: +label.text.name=Name: +label.text.difficulty=Schwierigkeit: +label.text.terrain=Gel\u00E4nde +label.text.placedBy=Platziert von: +label.text.owner=Betreut von: +label.text.date=Datum: +label.text.type=Typ: +label.text.container=Gr\u00F6\u00dfe: +label.text.shortdescription=Kurze Beschreibung: +label.text.htmldescription=HTML Beschreibung +label.text.longdescription=Lange Beschreibung: + +tab.text.descriptions=Beschreibungen +tab.text.general=Allgemein + +sort.cache.name = GC Code +sort.cache.type = Art +sort.cache.difficulty = Schwierigkeitsgrad +sort.cache.terrain = Gel\u00E4nde +sort.cache.placedBy = Platziert von +sort.cache.owner = Betreut von + +dialog.title.about = Über +dialog.title.new_list = Neue Liste +dialog.label.listname = Name der Liste: +dialog.msg.list.does.exist = Diese Liste existiert schon! +dialog.title.delete_list = Liste löschen + +all.caches = Alle Caches + +status.load.all.caches.from.db = Lade alle Caches von der Datenbank. +status.all.cache.lists.loaded = Alle Listen geladen. +status.load.caches.from.db = Lade Caches von der Datenbank. +status.all.caches.loaded = Alle Caches geladen. +status.store.all.caches = Speichere Caches in Datenbank. +status.all.caches.stored = Alle Caches in der Datenbank gespeichert. \ No newline at end of file diff --git a/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv_en.properties b/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv_en.properties new file mode 100644 index 0000000..41802fb --- /dev/null +++ b/src/main/resources/ninja/javafx/smartcsv/fx/smartcsv_en.properties @@ -0,0 +1,53 @@ +menu.title.import.gpx = Import GPX +menu.title.file = File +menu.title.quit = Quit +menu.title.help = Help +menu.title.settings = Settings +menu.title.about = About GeoFroggerFX +menu.title.plugins = Plugins +menu.title.list = Lists +menu.title.list.new = New list +menu.title.list.delete = Delete list + + +menu.title.sort = Sort +menu.title.filter = Filter + +label.text.cache.list=List: +label.text.cache.list.count=Number: +label.text.name=Name: +label.text.difficulty=Difficulty: +label.text.terrain=Terrain: +label.text.placedBy=Placed By: +label.text.owner=Owner: +label.text.date=Date: +label.text.type=Type: +label.text.container=Container: +label.text.shortdescription=Short description: +label.text.htmldescription=HTML Description +label.text.longdescription=Long description: + +tab.text.descriptions=Descriptions +tab.text.general=General + +sort.cache.name = GC Code +sort.cache.type = Type +sort.cache.difficulty = Difficulty +sort.cache.terrain = Terrain +sort.cache.placedBy = Placed by +sort.cache.owner = Owner + +dialog.title.about = About +dialog.title.new_list = New list +dialog.label.listname = Name of list: +dialog.msg.list.does.exist = List does already exist! +dialog.title.delete_list = Delete list + +all.caches = All caches + +status.load.all.caches.from.db = Load cache lists from database. +status.all.cache.lists.loaded = All cache lists loaded. +status.load.caches.from.db = Load caches from database. +status.all.caches.loaded = All caches loaded. +status.store.all.caches = Store caches in database. +status.all.caches.stored = All caches are stored in database. \ No newline at end of file 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 new file mode 100644 index 0000000..8ff6939 --- /dev/null +++ b/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVModelTest.java @@ -0,0 +1,103 @@ +/* + 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.table.model; + +import ninja.javafx.smartcsv.validation.Validator; +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * unit test for the csv model + */ +public class CSVModelTest { + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // constants + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + static final String TESTHEADER = "TESTHEADER"; + static final String TESTVALUE = "TESTVALUE"; + static final String FILEPATH = "filepath"; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // subject under test + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + CSVModel sut = new CSVModel(FILEPATH); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // tests + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + @Test + public void fresh_model_has_empty_rows() { + // assertion + assertThat(sut.getRows(), empty()); + } + + @Test + public void adds_a_new_row_into_row_list() { + // execution + CSVRow newRow = sut.addRow(); + + // assertion + assertThat(sut.getRows(), contains(newRow)); + } + + @Test + public void new_row_has_last_index_of_list_as_rownumber() { + // execution + CSVRow newRow = sut.addRow(); + + // assertion + assertThat(sut.getRows().indexOf(newRow), is(newRow.getRowNumber())); + } + + @Test + public void set_validator_calls_when_model_has_data() { + + // setup + Validator validator = mock(Validator.class); + setup_model_with_one_row_one_column_and_value(); + + // execution + sut.setValidator(validator); + + // assertion + verify(validator).isValid(TESTHEADER, TESTVALUE); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // private methods + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void setup_model_with_one_row_one_column_and_value() { + sut.setHeader(new String[] {TESTHEADER}); + sut.addRow().addValue(TESTHEADER, TESTVALUE); + } + +} \ No newline at end of file diff --git a/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVRowTest.java b/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVRowTest.java new file mode 100644 index 0000000..39230d8 --- /dev/null +++ b/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVRowTest.java @@ -0,0 +1,43 @@ +package ninja.javafx.smartcsv.fx.table.model; + +import ninja.javafx.smartcsv.validation.Validator; +import org.hamcrest.Matchers; +import org.junit.Test; + +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +/** + * unit test for row class + */ +public class CSVRowTest { + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // constants + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + static final String COLUMN = "COLUMN"; + static final String VALUE = "VALUE"; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // subject under test + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + CSVRow sut = new CSVRow(); + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // tests + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + @Test + public void adds_column_and_value_to_row() { + // execution + sut.addValue(COLUMN, VALUE); + + // assertion + assertThat(sut.getColumns(), hasKey(COLUMN)); + assertThat(sut.getColumns().get(COLUMN).get().getValue(), is(VALUE)); + } + + +} \ No newline at end of file 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 new file mode 100644 index 0000000..b2bff24 --- /dev/null +++ b/src/test/java/ninja/javafx/smartcsv/fx/table/model/CSVValueTest.java @@ -0,0 +1,58 @@ +package ninja.javafx.smartcsv.fx.table.model; + +import ninja.javafx.smartcsv.validation.Validator; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * unit test for the value class + */ +public class CSVValueTest { + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // constants + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + static final String COLUMN = "COLUMN"; + static final String VALUE = "VALUE"; + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // mocks + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Validator validator = mock(Validator.class); + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // subject under test + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + CSVValue sut; + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // init + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + @Before + public void initialize() { + sut = new CSVValue(); + sut.setValidator(validator); + } + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // tests + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + @Test + public void set_value_calls_validation() { + //setup + sut.setColumn(COLUMN); + + // execution + sut.setValue(VALUE); + + // assertion + verify(validator).isValid(COLUMN, VALUE); + } +} \ No newline at end of file diff --git a/src/test/java/ninja/javafx/smartcsv/validation/ValidatorTest.java b/src/test/java/ninja/javafx/smartcsv/validation/ValidatorTest.java new file mode 100644 index 0000000..d2ba076 --- /dev/null +++ b/src/test/java/ninja/javafx/smartcsv/validation/ValidatorTest.java @@ -0,0 +1,131 @@ +package ninja.javafx.smartcsv.validation; + +import com.typesafe.config.Config; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Collection; + +import static java.util.Arrays.asList; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * unit test for validator + */ +@RunWith(Parameterized.class) +public class ValidatorTest { + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // parameters + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private Config config; + private String column; + private String value; + private Boolean expectedResult; + private String expectedError; + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // subject under test + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private Validator sut; + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // parameterized constructor + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + public ValidatorTest(String configcolumn, + String configValidation, + Object configValue, + String column, + String value, + Boolean expectedResult, + String expectedError) { + this.config = config(configcolumn, configValidation, configValue); + this.column = column; + this.value = value; + this.expectedResult = expectedResult; + this.expectedError = expectedError; + } + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // init + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + @Before + public void initialize() { + sut = new Validator(config); + } + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // tests + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + @Test + public void validation() { + // execution + ValidationState result = sut.isValid(column, value); + + // assertion + assertThat(result.isValid(), is(expectedResult)); + if (!expectedResult) { + assertThat(result.error(), is(expectedError)); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // mocks + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private Config config(String column, String validation, Object value) { + + Config columnConfig = mock(Config.class); + Config validatorConfig = mock(Config.class); + + when(columnConfig.hasPath(column)).thenReturn(true); + when(columnConfig.getConfig(column)).thenReturn(validatorConfig); + + when(validatorConfig.hasPath(validation)).thenReturn(true); + if (value instanceof Boolean) { + when(validatorConfig.getBoolean(validation)).thenReturn((Boolean) value); + } else if (value instanceof String) { + when(validatorConfig.getString(validation)).thenReturn((String) value); + } else if (value instanceof Integer) { + when(validatorConfig.getInt(validation)).thenReturn((Integer)value); + } + + return columnConfig; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // parameters for tests + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + @Parameterized.Parameters + public static Collection validationConfigurations() { + return asList(new Object[][] { + { "column", "not empty", true, "column", "value", true, null }, + { "column", "not empty", true, "column", "", false, "should not be empty\n" }, + { "column", "not empty", true, "column", null, false, "should not be empty\n" }, + { "column", "integer", true, "column", "999", true, null }, + { "column", "integer", true, "column", "a", false, "should be an integer\n" }, + { "column", "minlength", 2, "column", "12", true, null }, + { "column", "minlength", 2, "column", "1", false, "has not min length of 2\n" }, + { "column", "maxlength", 2, "column", "12", true, null }, + { "column", "maxlength", 2, "column", "123", false, "has not max length of 2\n" }, + { "column", "date", "yyyyMMdd", "column", "20151127", true, null }, + { "column", "date", "yyyyMMdd", "column", "27.11.2015", false, "is not a date of format yyyyMMdd\n" }, + { "column", "alphanumeric", true, "column", "abcABC123", true, null }, + { "column", "alphanumeric", true, "column", "-abcABC123", false, "should not be alphanumeric\n" }, + { "column", "regexp", "[a-z]*", "column", "abc", true, null }, + { "column", "regexp", "[a-z]*", "column", "abcA", false, "does not match [a-z]*\n" }, + { "column", "groovy", "value.contains('a')? 'true' : 'no a inside'", "column", "abcdef", true, null }, + { "column", "groovy", "value.contains('a')? 'true' : 'no a inside'", "column", "bcdefg", false, "no a inside\n" }, + }); + } + + +} \ No newline at end of file