commit 3d28766fb5c4778e91c931e1b58a5ebf608e6da8 Author: egor Date: Sat Feb 12 14:16:24 2022 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a0a60d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Project exclude paths +/.gradle/ +/build/ +/build/classes/java/main/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..611e7c8 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..fdc392f --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5d98256 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..5f39c02 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + java +} + +group = "ru.erius" +version = "1.0" +val mainClass = "$group.${name.toLowerCase()}.$name" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") +} + +tasks.getByName("test") { + useJUnitPlatform() +} + +tasks.compileJava { + options.encoding = "UTF-8" +} + +tasks.jar { + manifest { + attributes( + "Manifest-Version" to "1.0", + "Main-Class" to mainClass + ) + } +} + +tasks.javadoc { + options.encoding = "UTF-8" +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..69a9715 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..744e882 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# 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\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# 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 +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +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" -a "$nonstop" = "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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # 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=`expr $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 + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..83cd3e8 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "Lab5" + diff --git a/src/main/java/ru/erius/lab5/Lab5.java b/src/main/java/ru/erius/lab5/Lab5.java new file mode 100644 index 0000000..3e8575b --- /dev/null +++ b/src/main/java/ru/erius/lab5/Lab5.java @@ -0,0 +1,18 @@ +package ru.erius.lab5; + +import ru.erius.lab5.cli.CommandParser; +import ru.erius.lab5.collection.PersonTreeSet; + +public class Lab5 { + /** + * Создание коллекции {@link PersonTreeSet PersonTreeSet} + * и парсера {@link CommandParser CommandParser}, запуск программы на выполнение + * + * @param args Аргументы командной строки + */ + public static void main(String[] args) { + PersonTreeSet pts = new PersonTreeSet(); + CommandParser cmd = new CommandParser(pts); + cmd.start(); + } +} diff --git a/src/main/java/ru/erius/lab5/cli/CommandParser.java b/src/main/java/ru/erius/lab5/cli/CommandParser.java new file mode 100644 index 0000000..d0c753c --- /dev/null +++ b/src/main/java/ru/erius/lab5/cli/CommandParser.java @@ -0,0 +1,597 @@ +package ru.erius.lab5.cli; + +import ru.erius.lab5.collection.*; + +import java.io.*; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Класс, добавляющий возможность интерактивного взаимодействия пользователя с коллекцией через терминал, + * для своей работы требует экземпляр класса {@link PersonTreeSet PersonTreeSet} + */ +public class CommandParser { + /** + * Вспомогательная строка для создания красивого вывода + */ + private final static String LINE = "============================================================================================================================================================================================="; + /** + * Строка, выводимая при запуске программы + */ + private final static String GREETINGS = LINE + "\n" + + "Добро пожаловать в программу для управления коллекцией объектов в интерактивном режиме!\n" + + "Напишите help, чтобы увидеть доступные команды\n" + + "Напишите exit, чтобы выйти из программы\n" + + LINE + "\n"; + /** + * Строка, выводимая при написании команды help + */ + private final static String HELP = LINE + "\n" + + "help: вывести справку по доступным командам\n" + + "info : вывести в стандартный поток вывода информацию о коллекции (тип, дата инициализации, количество элементов и т.д.)\n" + + "show : вывести в стандартный поток вывода все элементы коллекции в строковом представлении\n" + + "add [element] : добавить новый элемент в коллекцию\n" + + "update {id} [element] : обновить значение элемента коллекции, id которого равен заданному\n" + + "remove_by_id {id} : удалить элемент из коллекции по его id\n" + + "clear : очистить коллекцию\n" + + "save : сохранить коллекцию в файл\n" + + "execute_script {file_name} : считать и исполнить скрипт из указанного файла. В скрипте содержатся команды в таком же виде, в котором их вводит пользователь в интерактивном режиме.\n" + + "exit : завершить программу (без сохранения в файл)\n" + + "add_if_max [element] : добавить новый элемент в коллекцию, если его значение превышает значение наибольшего элемента этой коллекции\n" + + "add_if_min [element] : добавить новый элемент в коллекцию, если его значение меньше, чем у наименьшего элемента этой коллекции\n" + + "history : вывести последние 6 команд (без их аргументов)\n" + + "sum_of_height : вывести сумму значений поля height для всех элементов коллекции\n" + + "filter_contains_name {name} : вывести элементы, значение поля name которых содержит заданную подстроку\n" + + "print_field_descending_location : вывести значения поля location всех элементов в порядке убывания\n" + + LINE + "\n"; + /** + * Коллекция, на основе которой функционирует данный класс + */ + private final PersonTreeSet personTreeSet; + /** + * Логическая переменная, хранящая состояние программы + */ + private boolean isActive = false; + /** + * Очередь входных потоков, используется для выполнения пользовательских скриптов, + * также требуется для реализации возможности рекурсивного выполнения скриптов + */ + private final Queue inputStreams = new LinkedList<>(); + /** + * Reader, использующийся для чтения пользовательских команд или скриптов + */ + private BufferedReader reader; + /** + * Список последних 6 выполненных команд + */ + private final LinkedList history = new LinkedList<>(); + + /** + * Конструктор класса + * + * @param personTreeSet Коллекция типа {@link PersonTreeSet PersonTreeSet} + */ + public CommandParser(PersonTreeSet personTreeSet) { + this.personTreeSet = personTreeSet; + } + + /** + * Метод, открывающий поток ввода данных и запускающий программу, + * циклически вызывая функцию {@link #parse() parse} + */ + public void start() { + System.out.println(GREETINGS); + reader = new BufferedReader(new InputStreamReader(System.in)); + this.isActive = true; + do { + parse(); + } while (this.isActive); + } + + /** + * Основной метод, который ждет от пользователя ввода данных, после чего + * делит строку на команду и её аргументы, и в зависимости от них, + * вызывает нужную функцию на выполнение + */ + public void parse() { + String input = awaitInput("", "Что-то пошло не так").toLowerCase(Locale.ROOT); + String[] split = input.split(" "); + String command = split[0], argument = split.length > 1 ? split[1] : null; + switch (command) { + case "help": help(); break; + case "info": info(); break; + case "show": show(); break; + case "add": add(); break; + case "update": update(argument); break; + case "remove_by_id": removeByID(argument); break; + case "clear": clear(); break; + case "save": save(); break; + case "execute_script": executeScript(argument); break; + case "exit": exit(); break; + case "add_if_max": addIfMax(); break; + case "add_if_min": addIfMin(); break; + case "history": history(); break; + case "sum_of_height": sumOfHeight(); break; + case "filter_contains_name": filterContainsName(argument); break; + case "print_field_descending_location": printFieldDescendingLocation(); break; + default: unknown(); return; + } + updateHistory(command); + } + + /** + * Метод, обновляющий историю введенных команд + * + * @param command Введенная команда + */ + private void updateHistory(String command) { + history.add(command); + if (this.history.size() > 6) + this.history.removeFirst(); + } + + /** + * Метод, вызываемый при вводе неизвестной команды + */ + public void unknown() { + System.err.println("Неизвестная команда. Введите help для отображения списка всех команд"); + } + + /** + * Метод, вызываемый при вводе команды help, + * печатает содержимое строки {@link #HELP HELP} + */ + public void help() { + System.out.println(CommandParser.HELP); + } + + /** + * Метод, вызываемый при вводе команды info, + * печатает информацию о коллекции + */ + public void info() { + System.out.println(this.personTreeSet.info()); + } + + /** + * Метод, вызываемый при вводе команды show, + * печатает содержимое коллекции + */ + public void show() { + System.out.println("Вся коллекция PersonTreeSet:"); + System.out.println(this.personTreeSet.toString()); + } + + /** + * Метод, вызываемый при вводе команды add, + * пытается добавить новый элемент в коллекцию, + * печатает результат добавления + */ + public void add() { + Person person = createPerson(); + boolean success = this.personTreeSet.add(person); + String msg = success ? "Объект был добавлен в коллекцию" : "Не удалось добавить объект"; + System.out.println(msg); + } + + /** + * Метод, вызываемый при вводе команды update, + * пытается изменить существующий элемент коллекции по заданному id + * печатает результат изменения + * + * @param id Id человека + */ + public void update(long id) { + if (Person.getExistingPeople() < id || id <= 0) { + System.err.println("Не удалось найти объект с id " + id); + return; + } + Person person = createPerson(); + boolean success = this.personTreeSet.update(id, person); + String msg = success ? "Объект был успешно изменен" : "Не удалось найти объект с id " + id; + System.out.println(msg); + } + + /** + * Метод, аналогичный {@link #update(long) update(long)}, + * пытается преобразовать данную строку в Long и вызвать {@link #update(long) update(long)} + * + * @param strId Id человека типа String + */ + public void update(String strId) { + Long id = idOrNull(strId); + if (id != null) + update(id); + } + + /** + * Метод, вызываемый при вводе команды remove_by_id, + * пытается удалить существующий элемент из коллекции по заданному id + * печатает результат изменения + * + * @param id Id человека + */ + public void removeByID(long id) { + boolean success = this.personTreeSet.remove(id); + String msg = success ? "Объект был удален" : "Не удалось найти объект с id " + id; + System.out.println(msg); + } + + /** + * Метод, аналогичный {@link #removeByID(long) update(long)}, + * пытается преобразовать данную строку в Long и вызвать {@link #update(long) update(long)} + * + * @param strId Id человека типа String + */ + public void removeByID(String strId) { + Long id = idOrNull(strId); + if (id != null) + removeByID(id); + } + + /** + * Метод, преобразующий строковый id в Long, + * печатает результат преобразования + * + * @param strId Преобразуемая строка + * @return Преобразованная строка в Long или null при ошибке + */ + public Long idOrNull(String strId) { + long id; + if (strId == null) { + System.err.println("Параметр id должен быть указан"); + return null; + } + try { + id = Long.parseLong(strId); + } catch (NumberFormatException e) { + System.err.println("Параметр id должен являться целым числом"); + return null; + } + return id; + } + + /** + * Метод, вызываемый при вводе команды clear, очищает коллекцию + */ + public void clear() { + this.personTreeSet.clear(); + System.out.println("Коллекция успешно очищена"); + } + + /** + * Метод, вызываемый при вводе команды save, + * пытается сохранить коллекцию в файл, + * печатает результат сохранения + */ + public void save() { + boolean success = this.personTreeSet.save(); + String msg = success ? "Коллекция была сохранена в файле " + this.personTreeSet.getFile().getAbsolutePath() + : "Не удалось сохранить коллекцию (обычно причина пишется при запуске программы)"; + System.out.println(msg); + } + + /** + * Метод, вызываемый при вводе команды execute_script, + * пытается выполнить скрипт, написанный в файле fileName, + * печатает результат выполнения скрипта + * + * @param fileName Имя или путь к файлу + */ + public void executeScript(String fileName) { + if (fileName == null || fileName.isEmpty()) { + System.err.println("Параметр file_name должен быть указан"); + return; + } + File file = new File(fileName); + if (!file.exists() || file.isDirectory()) { + System.err.println("Не удалось найти указанный файл со скриптом"); + return; + } + InputStream stream; + try { + stream = new FileInputStream(file); + } catch (FileNotFoundException e) { + System.err.println("Файл не найден"); + return; + } + newInputStream(stream); + } + + /** + * Метод, вызываемый при вводе команды exit, + * заканчивает выполнение программы, предлагая перед этим сохранить коллекцию + */ + public void exit() { + if (this.personTreeSet.getFile() != null) { + String answer = awaitInput("Сохранить коллекцию в файл? (Y - да, N - нет)", "Введите Y или N", + input -> input.toUpperCase(Locale.ROOT).equals("Y") || input.toUpperCase(Locale.ROOT).equals("N"), + input -> input.toUpperCase(Locale.ROOT)); + if (answer.equals("Y")) + save(); + } + System.out.println("Выход из программы..."); + this.isActive = false; + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Метод, вызываемый при вводе команды add_if_max, + * пытается добавить новый элемент в коллекцию, только если он больше максимального элемента в коллекции + * печатает результат добавления + */ + public void addIfMax() { + Person person = createPerson(); + boolean success = this.personTreeSet.addIfMax(person); + String msg = success ? "Объект был добавлен в коллекцию" : "Не удалось добавить объект"; + System.out.println(msg); + } + + /** + * Метод, вызываемый при вводе команды add_if_min, + * пытается добавить новый элемент в коллекцию, только если он меньше минимального элемента в коллекции + * печатает результат добавления + */ + public void addIfMin() { + Person person = createPerson(); + boolean success = this.personTreeSet.addIfMin(person); + String msg = success ? "Объект был добавлен в коллекцию" : "Не удалось добавить объект"; + System.out.println(msg); + } + + /** + * Метод, вызываемый при вводе команды history, + * печатает историю последних 6 введенных команд + */ + public void history() { + System.out.println("История последних 6 команд:"); + this.history.forEach(System.out::println); + } + + /** + * Метод, вызываемый при вводе команды sum_of_height, + * печатает суммарный рост всех людей в коллекции + */ + public void sumOfHeight() { + int sum = this.personTreeSet.sumOfHeight(); + System.out.println("Сумма ростов всех людей в коллекции: " + sum); + } + + /** + * Метод, фильтрующий и печатающий всех людей, в имени которых имеется строка name, + * проверка не чувствительна к регистру + * + * @param name Строка, по которой происходит фильтрация + */ + public void filterContainsName(String name) { + if (name == null || name.isEmpty()) { + System.err.println("Параметр name должен быть указан"); + return; + } + List people = this.personTreeSet.filterContainsName(name); + System.out.println("Список людей, в имени которых содержится " + name + ":"); + people.forEach(System.out::println); + } + + /** + * Метод, печатающий отсортированный по убыванию список местоположений всех людей в коллекции + */ + public void printFieldDescendingLocation() { + List locations = this.personTreeSet.fieldDescendingLocation(); + System.out.println("Список локаций в порядке убывания:"); + locations.forEach(System.out::println); + } + + /** + * Метод, интерактивно создающий нового человека, получая каждое новое значение из потока ввода {@link #reader reader} + * + * @return Новый экземпляр класса {@link Person Person} + */ + private Person createPerson() { + System.out.println("Создание нового объекта класса Person"); + Person person = new Person(); + person.setName( + awaitInput("Введите имя:", "Введите непустую строку", + input -> !input.isEmpty()) + ); + person.setHeight( + awaitInput("Введите рост:", "Введите целое число, большее нуля", + input -> { + try { + int num = Integer.parseInt(input); + return num > 0; + } catch (NumberFormatException e) { + return false; + } + }, Integer::parseInt) + ); + person.setPassportID( + awaitInput("Введите номер паспорта:", "Введите как минимум 8 символов", + input -> input.length() >= 8) + ); + person.setEyeColor( + awaitInput("Введите цвет глаз (BLACK, ORANGE, BROWN):", "Введите один из предложенных цветов", + input -> Color.isColorValid(input.toUpperCase(Locale.ROOT)), input -> Color.valueOf(input.toUpperCase(Locale.ROOT))) + ); + person.setNationality( + awaitInput("Введите страну (UNITED_KINGDOM, GERMANY, CHINA, THAILAND, JAPAN):", "Введите одну из предлженных стран", + input -> Country.isCountryValid(input.toUpperCase(Locale.ROOT)), input -> Country.valueOf(input.toUpperCase(Locale.ROOT))) + ); + person.setCoordinates(createCoordinates()); + person.setLocation(createLocation()); + return person; + } + + /** + * Метод, интерактивно создающий новые координаты, получая каждое новое значение из потока ввода {@link #reader reader} + * + * @return Новый экземпляр класса {@link Coordinates Coordinates} + */ + private Coordinates createCoordinates() { + System.out.println("Создание нового объекта класса Coordinates"); + Coordinates coordinates = new Coordinates(); + coordinates.setX( + awaitInput("Введите x:", "Введите дробное число", + input -> { + try { + Float.parseFloat(input); + return true; + } catch (NumberFormatException e) { + return false; + } + }, Float::parseFloat) + ); + coordinates.setY( + awaitInput("Введите y:", "Введите дробное число, большее -816", + input -> { + try { + float num = Float.parseFloat(input); + return num > -816F; + } catch (NumberFormatException e) { + return false; + } + }, Float::parseFloat) + ); + return coordinates; + } + + /** + * Метод, интерактивно создающий новое местоположение, получая каждое новое значение из потока ввода {@link #reader reader} + * + * @return Новый экземпляр класса {@link Location Location} + */ + private Location createLocation() { + System.out.println("Создание нового объекта класса Location"); + Location location = new Location(); + location.setX( + awaitInput("Введите x:", "Введите дробное число", + input -> { + try { + Double.parseDouble(input); + return true; + } catch (NumberFormatException e) { + return false; + } + }, Double::parseDouble) + ); + location.setY( + awaitInput("Введите y:", "Введите дробное число", + input -> { + try { + Double.parseDouble(input); + return true; + } catch (NumberFormatException e) { + return false; + } + }, Float::parseFloat) + ); + location.setZ( + awaitInput("Введите z:", "Введите целое число", + input -> { + try { + Long.parseLong(input); + return true; + } catch (NumberFormatException e) { + return false; + } + }, Long::parseLong) + ); + location.setName( + awaitInput("Введите название:", "Введите непустую строку", + input -> !input.isEmpty()) + ); + return location; + } + + /** + * Метод, ожидающий ввода из потока ввода {@link #reader reader} и возвращающий результат,, + * печатает запрос msg перед ожиданием данных (если их вводит пользователь), + * печатает ошибку err, если при вводе данных произошла ошибка + * + * @param msg Строка, печатающаяся как запрос данных от пользователя + * @param err Строка, печатающаяся во время ошибки + * + * @return Строка из потока ввода + */ + private String awaitInput(String msg, String err) { + return awaitInput(msg, err, input -> true); + } + + /** + * Метод, ожидающий ввода из потока ввода {@link #reader reader } и возвращающий результат,, + * печатает запрос msg перед ожиданием данных (если их вводит пользователь), + * печатает ошибку err, если введенные данные не соответствуют предикату predicate + * + * @param msg Строка, печатающаяся как запрос данных от пользователя + * @param err Строка, печатающаяся при несоответствии ввода предикату + * @param predicate Предикат, определяющий валидность введенных данных + * + * @return Строка из потока ввода + */ + private String awaitInput(String msg, String err, Predicate predicate) { + String input = null; + do { + if (inputStreams.isEmpty()) + System.out.print(msg + "\n>>> "); + try { + input = reader.readLine(); + } catch (IOException e) { + e.printStackTrace(); + } + if (input == null) { + prevInputStream(); + continue; + } + if (predicate.test(input.trim())) + break; + else + System.err.println(err); + } while (true); + System.out.println(); + return input.trim(); + } + + /** + * Метод, ожидающий ввода из потока ввода {@link #reader reader} и возвращающий результат, + * печатает запрос msg перед ожиданием данных (если их вводит пользователь), + * печатает ошибку err, если введенные данные не соответствуют предикату predicate, + * преобразует результат в тип T в соответствии с функцией transform + * + * @param msg Строка, печатающаяся как запрос данных от пользователя + * @param err Строка, печатающаяся при несоответствии ввода предикату + * @param predicate Предикат, определяющий валидность введенных данных + * @param Тип, к которому будет приведен результат + * @param transform Функция, преобразующая результат в тип T + * + * @return Результат типа T + */ + private T awaitInput(String msg, String err, Predicate predicate, Function transform) { + String result = awaitInput(msg, err, predicate); + return transform.apply(result); + } + + /** + * Метод, меняющий текущий поток ввода на stream и добавляет его в очередь {@link #inputStreams inputStreams} + * + * @param stream Новый поток ввода + */ + private void newInputStream(InputStream stream) { + this.reader = new BufferedReader(new InputStreamReader(stream)); + this.inputStreams.add(stream); + } + + /** + * Метод, убирающий текущий поток ввода из очереди {@link #inputStreams inputStreams} + * и меняющий его либо на следующий в очереди поток, либо на System.in, если очередь пуста + */ + private void prevInputStream() { + inputStreams.poll(); + InputStream stream = inputStreams.isEmpty() ? System.in : inputStreams.peek(); + this.reader = new BufferedReader(new InputStreamReader(stream)); + } +} diff --git a/src/main/java/ru/erius/lab5/collection/Color.java b/src/main/java/ru/erius/lab5/collection/Color.java new file mode 100644 index 0000000..71db3a1 --- /dev/null +++ b/src/main/java/ru/erius/lab5/collection/Color.java @@ -0,0 +1,26 @@ +package ru.erius.lab5.collection; + +/** + * Перечисление цветов + */ +public enum Color { + BLACK, + ORANGE, + BROWN; + + /** + * Метод, проверяющий, можно ли из данной строки получить значение типа Color + * + * @param strColor Проверяемая строка + * + * @return true, если из строки можно получить значение типа Color, иначе false + */ + public static boolean isColorValid(String strColor) { + try { + Color.valueOf(strColor); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } +} diff --git a/src/main/java/ru/erius/lab5/collection/Coordinates.java b/src/main/java/ru/erius/lab5/collection/Coordinates.java new file mode 100644 index 0000000..c47c587 --- /dev/null +++ b/src/main/java/ru/erius/lab5/collection/Coordinates.java @@ -0,0 +1,88 @@ +package ru.erius.lab5.collection; + +import java.util.Locale; + +/** + * Класс данных координат + */ +public class Coordinates { + + /** + * Координата X типа float + */ + private float x; + /** + * Координата Y типа float, значение должно быть больше -816 + */ + private float y; + + /** + * Конструктор без параметров, задаёт значения всех полей по умолчанию, + * x = 0, y = 0 + */ + public Coordinates() { + this.x = 0F; + this.y = 0F; + } + + /** + * Конструктор с параметрами + * + * @param x Координата X + * @param y Координата Y + * + * @throws IllegalArgumentException Если Y меньше или равен -816 + */ + public Coordinates(float x, float y) { + this.x = x; + this.y = y; + } + + public float getX() { + return x; + } + + public float getY() { + return y; + } + + public void setX(float x) { + this.x = x; + } + + /** + * Сеттер для поля y + * + * @param y Координата Y + * + * @throws IllegalArgumentException Если Y меньше или равен -816 + */ + public void setY(float y) { + this.y = y; + if (y <= -816) + throw new IllegalArgumentException("Поле y класса Coordinates должно быть больше -816"); + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + + Coordinates that = (Coordinates) other; + + if (Float.compare(that.x, x) != 0) return false; + return Float.compare(that.y, y) == 0; + } + + @Override + public int hashCode() { + int result = (x != 0.0f ? Float.floatToIntBits(x) : 0); + result = 31 * result + (y != 0.0f ? Float.floatToIntBits(y) : 0); + return result; + } + + @Override + public String toString() { + return String.format(Locale.US, "Coordinates(x=%f, y=%f)", this.x, this.y); + } +} diff --git a/src/main/java/ru/erius/lab5/collection/Country.java b/src/main/java/ru/erius/lab5/collection/Country.java new file mode 100644 index 0000000..fe4b328 --- /dev/null +++ b/src/main/java/ru/erius/lab5/collection/Country.java @@ -0,0 +1,28 @@ +package ru.erius.lab5.collection; + +/** + * Перечисление стран + */ +public enum Country { + UNITED_KINGDOM, + GERMANY, + CHINA, + THAILAND, + JAPAN; + + /** + * Метод, проверяющий, можно ли из данной строки получить значение типа Country + * + * @param strCountry Проверяемая строка + * + * @return true, если из строки можно получить значение типа Country, иначе false + */ + public static boolean isCountryValid(String strCountry) { + try { + Country.valueOf(strCountry); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } +} diff --git a/src/main/java/ru/erius/lab5/collection/Location.java b/src/main/java/ru/erius/lab5/collection/Location.java new file mode 100644 index 0000000..43ab13d --- /dev/null +++ b/src/main/java/ru/erius/lab5/collection/Location.java @@ -0,0 +1,160 @@ +package ru.erius.lab5.collection; + +import java.util.Comparator; +import java.util.Locale; +import java.util.Objects; + +/** + * Класс данных местоположения, реализует сортировку по умолчанию + * по имени и расстоянию до точки (0; 0; 0) + */ +public class Location implements Comparable { + + /** + * Координата X типа double + */ + private double x; + /** + * Координата Y типа float + */ + private float y; + /** + * Координата Z типа Long, не может быть null + */ + private Long z; + /** + * Имя локации, может быть null + */ + private String name; + + /** + * Конструктор без параметров, задаёт значения всех полей по умолчанию, + * x = 0, y = 0, z = 0, name = null + */ + public Location() { + this.x = 0D; + this.y = 0F; + this.z = 0L; + this.name = null; + } + + /** + * Конструктор с параметрами + * + * @param x Координата X + * @param y Координата Y + * @param z Координата Z + * @param name Имя локации + * + * @throws IllegalArgumentException Данное исключение будет брошено в следующих случаях: + * Z является null, + * name является пустой строкой + */ + public Location(double x, float y, Long z, String name) { + this.x = x; + this.y = y; + this.setZ(z); + this.setName(name); + } + + public double getX() { + return x; + } + + public float getY() { + return y; + } + + public Long getZ() { + return z; + } + + public String getName() { + return name; + } + + public void setX(double x) { + this.x = x; + } + + public void setY(float y) { + this.y = y; + } + + /** + * Сеттер для поля z + * @param z Координата Z + * + * @throws IllegalArgumentException Если Z является null + */ + public void setZ(Long z) { + this.z = z; + if (z == null) + throw new IllegalArgumentException("Поле z класса Location не может быть null"); + } + + /** + * Сеттер для поля name + * @param name Имя локации + * + * @throws IllegalArgumentException Если name является пустой строкой + */ + public void setName(String name) { + this.name = name; + if (name.isEmpty()) + throw new IllegalArgumentException("Поле name класса Location не может быть пустым"); + } + + /** + * Переопределенный метод сравнения двух местоположений, + * сравнение производится по имени локации и расстоянию до точки (0; 0; 0) + * + * @param other Объект для сравнения + * @return Целое число - результат сравнения + */ + @Override + public int compareTo(Location other) { + return Comparator.comparing(Location::getName) + .thenComparing(Location::distance) + .compare(this, other); + } + + /** + * Метод, вычисляющий расстояние до точки (0; 0; 0) + * + * @return Расстояние типа double + */ + private double distance() { + return Math.sqrt(x * x + y * y + z * z); + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + + Location location = (Location) other; + + if (Double.compare(location.x, x) != 0) return false; + if (Float.compare(location.y, y) != 0) return false; + if (!z.equals(location.z)) return false; + return Objects.equals(name, location.name); + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(x); + result = (int) (temp ^ (temp >>> 32)); + result = 31 * result + (y != 0.0f ? Float.floatToIntBits(y) : 0); + result = 31 * result + z.hashCode(); + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return String.format(Locale.US, "Location(x=%f, y=%f, z=%s, name=%s)", this.x, this.y, this.z, this.name); + } +} diff --git a/src/main/java/ru/erius/lab5/collection/Person.java b/src/main/java/ru/erius/lab5/collection/Person.java new file mode 100644 index 0000000..2148b4c --- /dev/null +++ b/src/main/java/ru/erius/lab5/collection/Person.java @@ -0,0 +1,337 @@ +package ru.erius.lab5.collection; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Класс данных человека, реализует сортировку по умолчанию по имени, номеру паспорта, + * росту, национальности, местоположению и цвету глаз + */ +public class Person implements Comparable { + + /** + * Количество созданных людей, используется для задания + * уникального id для каждого объекта данного класса + */ + private static long existingPeople = 0; + + /** + * Регулярное выражение для валидации и создания + * экземпляров данного класса из строк + */ + private final static Pattern PERSON_REGEX = Pattern.compile( + "Person\\(\\s*id=(?\\d+),\\s*name=(?\\S+)," + + "\\s*coordinates=Coordinates\\(x=(?[+-]?(\\d+([.,]\\d*)?|[.,]\\d+)),\\s*y=(?[+-]?(\\d+([.,]\\d*)?|[.,]\\d+))\\)," + + "\\s*creationDate=(?\\d{4}[\\-.]\\d{2}[\\-.]\\d{2}),\\s*height=(?\\d+),\\s*passportID=(?\\S{8,})," + + "\\s*eyeColor=(?\\w+),\\s*nationality=(?\\w+),\\" + + "s*location=Location\\(x=(?[+-]?(\\d+([.,]\\d*)?|[.,]\\d+)),\\s*y=(?[+-]?(\\d+([.,]\\d*)?|[.,]\\d+))," + + "\\s*z=(?[+-]?\\d+),\\s*name=(?\\S+)\\)\\)" + ); + + /** + * Id человека, не может быть null, значение поля должно быть больше 0, + * значение этого поля должно быть уникальным, значение этого поля должно генерироваться автоматически + */ + private Long id; + /** + * Имя человека, не может быть null, строка не может быть пустой + */ + private String name; + /** + * Координаты человека, не может быть null + */ + private Coordinates coordinates; + /** + * Дата создания объекта, не может быть null, значение этого поля должно генерироваться автоматически + */ + private LocalDate creationDate; + /** + * Рост человека, может быть null, значение поля должно быть больше 0 + */ + private Integer height; + /** + * Номер паспорта человека, длина строки должна быть не меньше 8, поле может быть null + */ + private String passportID; + /** + * Цвет глаз человека, не может быть null + */ + private Color eyeColor; + /** + * Национальность человека, не может быть null + */ + private Country nationality; + /** + * Местоположение человека, может быть null + */ + private Location location; + + /** + * Конструктор без параметров, задаёт значения всех полей по умолчанию, + * name = "None", coordinates = new Coordinates(), height = 1, + * passportID = null, eyeColor = Color.BLACK, nationality = Country.UNITED_KINGDOM + */ + public Person() { + this.id = ++existingPeople; + this.creationDate = LocalDate.now(); + this.location = null; + this.name = "None"; + this.coordinates = new Coordinates(); + this.height = 1; + this.passportID = null; + this.eyeColor = Color.BLACK; + this.nationality = Country.UNITED_KINGDOM; + } + + /** + * Конструктор с параметрами + * + * @param name Имя человека + * @param coordinates Координаты человека + * @param height Высота человека + * @param passportID Номер паспорта человека + * @param eyeColor Цвет глаз человека + * @param nationality Национальность человека + * @param location Местоположение человека + * + * @throws IllegalArgumentException Данное исключение будет брошено в следующих случаях: + * name является null или пустой строкой, + * coordinates является null, + * height меньше 0, + * Длина passportID меньше 8 символов, + * eyeColor является null, + * nationality является null + */ + public Person(String name, Coordinates coordinates, Integer height, String passportID, + Color eyeColor, Country nationality, Location location) { + this.id = ++existingPeople; + this.creationDate = LocalDate.now(); + this.location = location; + this.setName(name); + this.setCoordinates(coordinates); + this.setHeight(height); + this.setPassportID(passportID); + this.setEyeColor(eyeColor); + this.setNationality(nationality); + } + + public static long getExistingPeople() { + return existingPeople; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Coordinates getCoordinates() { + return coordinates; + } + + public Integer getHeight() { + return height; + } + + public String getPassportID() { + return passportID; + } + + public Color getEyeColor() { + return eyeColor; + } + + public Country getNationality() { + return nationality; + } + + public Location getLocation() { + return location; + } + + /** + * Сеттер для поля name + * + * @param name Имя человека + * + * @throws IllegalArgumentException Если имя является null или пустой строкой + */ + public void setName(String name) { + this.name = name; + if (name == null || name.isEmpty()) + throw new IllegalArgumentException("Поле name класса Person не может быть null или пустым"); + } + + /** + * Сеттер для поля coordinates + * + * @param coordinates Координаты человека + * + * @throws IllegalArgumentException Если объект координат является null + */ + public void setCoordinates(Coordinates coordinates) { + this.coordinates = coordinates; + if (coordinates == null) + throw new IllegalArgumentException("Поле coordinates класса Person не может быть null"); + } + + /** + * Сеттер для поля height + * + * @param height Рост человека + * + * @throws IllegalArgumentException Если рост меньше 0 + */ + public void setHeight(Integer height) { + this.height = height; + if (height <= 0) + throw new IllegalArgumentException("Поле height класса Person должно быть больше 0"); + } + + /** + * Сеттер для поля passportID + * + * @param passportID Номер паспорта человека + * + * @throws IllegalArgumentException Если номер паспорта меньше 8 символов в длину + */ + public void setPassportID(String passportID) { + this.passportID = passportID; + if (passportID.length() < 8) + throw new IllegalArgumentException("Поле passportID класса Person не может быть меньше 8 символов в длину"); + } + + /** + * Сеттер для поля eyeColor + * + * @param eyeColor Координаты человека + * + * @throws IllegalArgumentException Если цвет глаз является null + */ + public void setEyeColor(Color eyeColor) { + this.eyeColor = eyeColor; + if (eyeColor == null) + throw new IllegalArgumentException("Поле eyeColor класса Person не может быть null"); + } + + /** + * Сеттер для поля nationality + * + * @param nationality Координаты человека + * + * @throws IllegalArgumentException Если национальность является null + */ + public void setNationality(Country nationality) { + this.nationality = nationality; + if (nationality == null) + throw new IllegalArgumentException("Поле nationality класса Person не может быть null"); + } + + public void setLocation(Location location) { + this.location = location; + } + + /** + * Статический метод, возвращающий список объектов Person, + * образованный из найденных в строке совпадений с + * регулярным выражением {@link #PERSON_REGEX PERSON_REGEX} + * + * @param str Строка, из которой требуется получить список людей + * @return Список объектов класса Person + */ + public static List peopleFromString(String str) { + List list = new ArrayList<>(); + Matcher matcher = PERSON_REGEX.matcher(str); + while (matcher.find()) { + try { + Person person = new Person(); + person.id = Long.parseLong(matcher.group("id")); + person.name = matcher.group("name"); + person.height = Integer.parseInt(matcher.group("height")); + person.passportID = matcher.group("passportID"); + person.eyeColor = Color.valueOf(matcher.group("eyeColor")); + person.nationality = Country.valueOf(matcher.group("nationality")); + person.coordinates = new Coordinates( + Float.parseFloat(matcher.group("coordX")), + Float.parseFloat(matcher.group("coordY")) + ); + person.location = new Location( + Double.parseDouble(matcher.group("locX")), + Float.parseFloat(matcher.group("locY")), + Long.parseLong(matcher.group("locZ")), + matcher.group("locName") + ); + String[] date = matcher.group("creationDate").split("[\\-.]"); + person.creationDate = LocalDate.of( + Integer.parseInt(date[0]), + Integer.parseInt(date[1]), + Integer.parseInt(date[2]) + ); + list.add(person); + } catch (NumberFormatException | DateTimeException e) { + e.printStackTrace(); + System.err.println("Что-то пошло не так при чтении данных из файла, не меняйте вручную данные в файле!!!"); + } + } + return list; + } + + /** + * Переопределенный метод сравнения двух людей, + * сравнение производится по имени, номеру пасспорта, + * росту, национальности, местоположению и цвету глаз + * + * @param other Объект для сравнения + * @return Целое число - результат сравнения + */ + @Override + public int compareTo(Person other) { + return Comparator.comparing(Person::getName) + .thenComparing(Person::getPassportID) + .thenComparing(Person::getHeight) + .thenComparing(Person::getNationality) + .thenComparing(Person::getLocation) + .thenComparing(Person::getEyeColor) + .compare(this, other); + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + + Person person = (Person) other; + + if (!name.equals(person.name)) return false; + if (!coordinates.equals(person.coordinates)) return false; + if (!Objects.equals(height, person.height)) return false; + if (!Objects.equals(passportID, person.passportID)) return false; + if (eyeColor != person.eyeColor) return false; + if (nationality != person.nationality) return false; + return Objects.equals(location, person.location); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + coordinates.hashCode(); + result = 31 * result + (height != null ? height.hashCode() : 0); + result = 31 * result + (passportID != null ? passportID.hashCode() : 0); + result = 31 * result + eyeColor.hashCode(); + result = 31 * result + nationality.hashCode(); + result = 31 * result + (location != null ? location.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return String.format(Locale.US, "Person(id=%d, name=%s, coordinates=%s, creationDate=%s, height=%s, passportID=%s, " + + "eyeColor=%s, nationality=%s, location=%s)", this.id, this.name, this.coordinates, this.creationDate, + this.height, this.passportID, this.eyeColor, this.nationality, this.location); + } +} diff --git a/src/main/java/ru/erius/lab5/collection/PersonTreeSet.java b/src/main/java/ru/erius/lab5/collection/PersonTreeSet.java new file mode 100644 index 0000000..cfce359 --- /dev/null +++ b/src/main/java/ru/erius/lab5/collection/PersonTreeSet.java @@ -0,0 +1,295 @@ +package ru.erius.lab5.collection; + +import java.io.*; +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Коллекция PersonTreeSet, использующая в основе своей работы коллекцию {@link TreeSet TreeSet}, + * имеет возможности записи коллекции в файл и чтения её же из файла + */ +public class PersonTreeSet { + /** + * Название переменной окружения, где должен быть указан путь к файлу + */ + private final static String ENV_VAR = "PTS_PATH"; + /** + * Тип коллекции, используется для вывода информации + */ + private final static String TYPE = "TreeSet"; + + /** + * Файл для записи и чтения коллекции, является null если путь к файлу некорректный + */ + private final File file = initFile(); + /** + * Логические переменные, определяющие возможность программы записывать в файл или читать из него + */ + private boolean canWrite = true, canRead = true; + /** + * Коллекция {@link TreeSet TreeSet}, лежащая в основе работы класса + */ + private final TreeSet set = new TreeSet<>(); + /** + * Дата инициализации коллекции + */ + private final LocalDate creationDate; + + /** + * Конструктор без параметров, инициализирует коллекцию из файла (если есть возможность) + * и задаёт дату инициализации + */ + public PersonTreeSet() { + this.creationDate = LocalDate.now(); + if (this.file != null && this.canRead) + this.initTreeSet(); + } + + /** + * Метод, возвращающий файл для записи и чтения коллекции, + * возвращает null если переменной окружении не существует, + * файла не существует, файл является директорией + * или имеет расширение, не являющееся .xml + * + * Устанавливает соответствующим полям {@link #canRead canRead} и {@link #canWrite canWrite} значения false, + * если нет прав на чтение или запись + * + * @return Файл, если все условия соблюдены, иначе null + */ + public File initFile() { + String path = System.getenv(ENV_VAR); + if (path == null) { + System.err.println("Переменная окружения " + ENV_VAR + " (путь к файлу) не задана, возможность сохранения и загрузки коллекции в файл отключена"); + return null; + } + + File file = new File(path); + if (!file.exists()) { + System.err.println("Файла по пути " + path + " не существует, возможность сохранения и загрузки коллекции в файл отключена"); + return null; + } + + if (!file.isFile()) { + System.err.println("В " + ENV_VAR + " записан путь к директории, возможность сохранения и загрузки коллекции в файл отключена"); + return null; + } + + String ext = this.getFileExtension(file.getPath()); + if (ext == null || !ext.equals("xml")) { + System.err.println("Файл должен иметь расширение xml, возможность сохранения и загрузки коллекции в файл отключена"); + return null; + } + + if (!file.canWrite()) { + System.err.println("Запись в этот файл невозможна, возможность сохранения коллекции файл отключена"); + this.canWrite = false; + } + + if (!file.canRead()) { + System.err.println("Чтение из этого файл невозможно, возможность загрузки коллекции из файла отключена"); + this.canRead = false; + } + + System.out.println("Файл для сохранения и загрузки коллекции был успешно найден"); + System.out.println("Путь: " + path); + + return file; + } + + /** + * Метод инициализации коллекции из файла, + * читает содержимое файла и передаёт его в метод {@link Person#peopleFromString(String)}, + * элементы полученного списка добавляются в коллекцию + */ + private void initTreeSet() { + StringBuilder fileContents = new StringBuilder(); + try (InputStreamReader reader = new InputStreamReader(new FileInputStream(this.file))) { + int read; + do { + char[] buffer = new char[8192]; + read = reader.read(buffer); + fileContents.append(buffer); + } while (read != -1); + } catch (IOException e) { + e.printStackTrace(); + System.err.println("Не удалось инициализировать коллекцию из файла"); + return; + } + String contents = fileContents.toString(); + List people = Person.peopleFromString(contents); + this.set.addAll(people); + System.out.println("Коллекция успешно инициализирована из файла"); + } + + public File getFile() { + return file; + } + + /** + * Метод, возвращающий расширение файла + * + * @param path Строка, в которой записан путь к файлу + * + * @return Расширение файла или null, если путь к файлу некорректный + */ + private String getFileExtension(String path) { + String[] split = path.split("\\.", 2); + return split.length > 1 ? split[1] : null; + } + + /** + * Метод, возвращающий строку с информацией о коллекции + * + * @return Строка с информацией о коллекции + */ + public String info() { + return String.format("Тип коллекции: %s \n" + + "Дата инициализации: %s \n" + + "Количество элементов: %d \n" + + "Путь к файлу хранения: %s", + PersonTreeSet.TYPE, this.creationDate, this.set.size(), this.file); + } + + /** + * Метод добавления нового элемента в коллекцию + * + * @param person Элемент для добавления + * + * @return true, если элемент был успешно добавлен, иначе false + */ + public boolean add(Person person) { + return this.set.add(person); + } + + /** + * Метод изменения элемента в коллекции по заданному id + * + * @param id Id человека, которого нужно изменить + * @param person Элемент для добавления + * + * @return true, если элемент был успешно найден и изменён, иначе false + */ + public boolean update(long id, Person person) { + Optional element = this.set.stream().filter(p -> p.getId() == id).findAny(); + if (element.isEmpty()) return false; + Person p = element.get(); + p.setName(person.getName()); + p.setCoordinates(person.getCoordinates()); + p.setHeight(person.getHeight()); + p.setPassportID(person.getPassportID()); + p.setEyeColor(person.getEyeColor()); + p.setNationality(person.getNationality()); + p.setLocation(person.getLocation()); + return true; + } + + /** + * Метод удаления элемента из коллекции по заданному id + * + * @param id Id человека, которого нужно удалить + * + * @return true, если элемент был успешно найден и удален, иначе false + */ + public boolean remove(long id) { + return this.set.removeIf(p -> p.getId() == id); + } + + /** + * Метод очистки коллекции + */ + public void clear() { + this.set.clear(); + } + + /** + * Метод сохранения коллекции в файл + * + * @return true, если коллекция была успешно сохранена, иначе false + */ + public boolean save() { + if (file == null || !this.canWrite) + return false; + try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file))) { + writer.write(this.toString()); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } + + /** + * Метод добавления элемента в коллекцию, если он больше её максимального элемента + * + * @param person Элемент для добавления + * + * @return true, если элемент был успешно добавлен, иначе false + */ + public boolean addIfMax(Person person) { + Person last = this.set.last(); + if (person.compareTo(last) > 0) + return this.set.add(person); + return false; + } + + /** + * Метод добавления элемента в коллекцию, если он меньше её минимального элемента + * + * @param person Элемент для добавления + * + * @return true, если элемент был успешно добавлен, иначе false + */ + public boolean addIfMin(Person person) { + Person first = this.set.first(); + if (person.compareTo(first) < 0) + return this.set.add(person); + return false; + } + + /** + * Метод, складывающий рост каждого человека в коллекции + * + * @return Суммарный рост всех людей + */ + public int sumOfHeight() { + return this.set.stream() + .mapToInt(Person::getHeight) + .sum(); + } + + /** + * Метод, фильтрующий всех людей, в имени которых имеется строка name, + * проверка не чувствительна к регистру + * + * @param name Строка, по которой происходит фильтрация + * + * @return Список людей, в имени которых имеется строка name + */ + public List filterContainsName(String name) { + return this.set.stream() + .filter(p -> p.getName().toLowerCase(Locale.ROOT).contains(name.toLowerCase(Locale.ROOT))) + .collect(Collectors.toList()); + } + + /** + * Метод, возвращающий отсортированный по убыванию список местоположений всех людей в коллекции + * + * @return Отсортированный по убыванию список местоположений + */ + public List fieldDescendingLocation() { + return this.set.stream() + .map(Person::getLocation) + .sorted(Collections.reverseOrder()) + .collect(Collectors.toList()); + } + + @Override + public String toString() { + return "PersonTreeSet(\n" + + this.set.stream() + .map(p -> "\t" + p.toString()) + .collect(Collectors.joining(",\n")) + + "\n)"; + } +}