From fd63b81d223c99129d523fa4690181df2417b1d0 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 10 Jun 2023 23:59:27 +0300 Subject: [PATCH] Added project --- .editorconfig | 31 +++ .gitignore | 121 ++++++++++- README.md | 75 ++++++- pom.xml | 191 ++++++++++++++++++ .../kotlin/ru/resprojects/exceltojson/Main.kt | 152 ++++++++++++++ src/main/resources/log4j.properties | 5 + .../ru/resprojects/exceltojson/MainTest.kt | 28 +++ src/test/resources/xlsx/test.xlsx | Bin 0 -> 9450 bytes 8 files changed, 597 insertions(+), 6 deletions(-) create mode 100644 .editorconfig create mode 100644 pom.xml create mode 100644 src/main/kotlin/ru/resprojects/exceltojson/Main.kt create mode 100644 src/main/resources/log4j.properties create mode 100644 src/test/kotlin/ru/resprojects/exceltojson/MainTest.kt create mode 100644 src/test/resources/xlsx/test.xlsx diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0599cdc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,31 @@ +# EditorConfig is awesome: https://EditorConfig.org + +root = true + +[*] +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 160 + +[*.java] +indent_style = space +indent_size = 4 +#https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#ideas-for-domain-specific-properties +wildcard_import_limit = 100 +#https://github.com/ec4j/editorconfig-java-domain +java_class_count_to_use_import_on_demand = 100 +#https://youtrack.jetbrains.com/issue/IDEA-212525#focus=streamItem-27-3521523.0-0 +#https://www.jetbrains.com/help/idea/configuring-code-style.html +ij_java_names_count_to_use_import_on_demand = 100 + +[*.json] +indent_style = space +indent_size = 2 + +[*.xml] +indent_style = space +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore index 1b938a6..bfe643a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ -# ---> Kotlin +# Created by https://www.gitignore.io/api/java,maven,jetbrains +# Edit at https://www.gitignore.io/?templates=java,maven,jetbrains + +### Java ### # Compiled class file *.class @@ -22,5 +25,119 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* -replay_pid* +### JetBrains ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/**/sonarlint/ + +# SonarQube Plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator/ + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.flattened-pom.xml + +# End of https://www.gitignore.io/api/java,maven,jetbrains + +.idea/ +*.iml +*.ipr +*.sh + +*.log.* +application.properties +application.yaml +/config/ +/k8s-ansible/ +/Dockerfile +/build/ diff --git a/README.md b/README.md index 52bab9b..db821fa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,73 @@ # ExcelToJson -1. Утилита считывает поданный на вход xlsx-файл, путь к которому указывается в виде значения параметра при запуске утилиты. -2. Последовательно считываются строки и столбцы, создаётся объект с заданным набором полей и заносятся в список. -3. Список маршаллизуется и сохраняется в файл формата json. Файл имеет то же название, что и xlsx-документ и помещается в тот же -каталог. \ No newline at end of file +1. Утилита считывает поданный на вход xlsx-файл, путь к которому указывается в виде значения параметра при запуске утилиты. +2. Последовательно считываются строки и столбцы, создаётся объект с заданным набором полей и заносятся в список. +3. Список маршаллизуется и сохраняется в файл формата json. Файл имеет то же название, что и xlsx-документ и помещается в тот же каталог. + +**Примечания** + +***Формат xlsx-документа*** + +Подаваемый на вход xlsx-документ состоит из одного листа (sheet) и имеет заданный набор столбцов, первая строка это заголовок столбцов, все последующие строки - значения столбцов. Значения в первом столбце заданы в числовом формате типа integer, значения остальных столбцов заданы в строковом формате. + +Пример: + +| id | field | value | value_map | +| ------ | ------ | ------ | ------ | +| int | string | string | string | + +***Формат json*** + +``` +{ + "id": 1, + "field": "string", + "value": "string", + "value_map": "string" +} +``` + +**Расширенная опция** + +В дополнении к основной функциональности так же есть возможность прямой записи содержимого xlsx-файл в виде структуры в mongo db. + +Структура записи mongo db имеет вид + +``` +{ + "id": 1, + "field": "string", + "value": "string", + "value_map": "string" +} +``` + +**Инструкция по использованию** + +Для запуска консольной программы exceltojson.jar необходимо наличие jdk не ниже версии 8 + +Программа работает в двух режимах, экспорт в json (по умолчанию) и экспорт в mongo db (нужно задать ключ при запуске программы) + +***Общее описание запуска программы (экспорт в json)*** + +`java -jar exceltojson.jar <полный_путь_к_файлу_формата_xlsx>` + +на выходе будет файл формата json, имеющий то же название что и входящий файл и будет располагаться в том же каталоге где и входящий файл + +Общее описание запуска программы (экспорт в mongo db) + +`java -jar exceltojson.jar --mongo=mongodb://:// <полный_путь_к_файлу_формата_xlsx>` + +***Примеры запуска*** + +****Экспорт в json**** + +`java -jar exceltojson.jar c:\myfolder\myfile.xlsx` + +На выходе получим json `c:\myfolder\myfile.json` + +****Экспорт в mongo db**** + +`java -jar exceltojson.jar --mongo=mongodb://mongo-01.testbanki.ru:27017/dict/dictMap c:\myfolder\myfile.xlsx` + +Данные из myfile.xlsx экспортируются в mongo db в указанную коллекцию dictMap базы данных dict diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..16f5e4b --- /dev/null +++ b/pom.xml @@ -0,0 +1,191 @@ + + + 4.0.0 + + exceltojson + ru.resprojects + 1.0.0.2 + jar + + ExcelToJson + + + UTF-8 + official + 1.8 + ru.resprojects.exceltojson.MainKt + + + + + mavenCentral + https://repo1.maven.org/maven2/ + + + + + + org.jetbrains.kotlin + kotlin-test-junit5 + 1.5.10 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.6.0 + test + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.5.10 + + + + org.apache.poi + poi + 5.0.0 + + + org.apache.poi + poi-ooxml + 5.0.0 + + + + com.fasterxml.jackson.module + jackson-module-kotlin + 2.12.4 + + + + io.github.microutils + kotlin-logging-jvm + 2.0.8 + + + org.slf4j + slf4j-api + 1.7.32 + + + org.slf4j + slf4j-log4j12 + 1.7.32 + + + + org.litote.kmongo + kmongo + 4.2.8 + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + 1.5.10 + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + MainKt + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + make-assembly + package + + single + + + + + ${main.class} + + + + jar-with-dependencies + + + + + + + com.akathist.maven.plugins.launch4j + launch4j-maven-plugin + 2.1.2 + + + l4j-clui + package + launch4j + + console + target/${project.artifactId}.exe + target/${project.artifactId}-${project.version}-jar-with-dependencies.jar + exceltojson + + 1.8 + %JAVA_HOME% + 128 + 512 + + + ${project.version} + ${project.version} + ${project.name} + MIT + ${project.version} + ${project.version} + ${project.name} + exceltojson.exe + + exceltojson + + + + + + + + + diff --git a/src/main/kotlin/ru/resprojects/exceltojson/Main.kt b/src/main/kotlin/ru/resprojects/exceltojson/Main.kt new file mode 100644 index 0000000..a5acdce --- /dev/null +++ b/src/main/kotlin/ru/resprojects/exceltojson/Main.kt @@ -0,0 +1,152 @@ +package ru.resprojects.exceltojson + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.mongodb.ConnectionString +import com.mongodb.client.MongoCollection +import mu.KotlinLogging +import org.apache.commons.collections4.list.UnmodifiableList +import org.apache.poi.ss.usermodel.CellType +import org.apache.poi.xssf.usermodel.XSSFSheet +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import org.litote.kmongo.KMongo +import org.litote.kmongo.findOne +import org.litote.kmongo.getCollection +import java.io.File +import java.net.URI +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Paths +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +private val logger = KotlinLogging.logger {} +private val mapper = jacksonObjectMapper() + +fun main(args: Array) { + if (args.isEmpty()) { + println(""" + usage exceltojson for generate json file + usage exceltojson --mongo=mongodb://:// for export to mongo + """.trimIndent()) + return + } + if (args.first().startsWith("--mongo=")) { + excelToMongo(args, URI(args.first().substring("--mongo=".length))) + } else { + args.forEach { + if (!it.startsWith("--")) { + println("Start process file $it") + excelToJson(it) + println("------") + } + } + } +} + +fun excelToMongo(fileNames: Array, mongoUri: URI) { + try { + val client = KMongo.createClient(ConnectionString(mongoUri.scheme + "://" + mongoUri.host + ":" + mongoUri.port)) + client.use { mongoClient -> + val database = mongoClient.getDatabase(mongoUri.path.removePrefix("/").split("/")[0]) + val collectionName = mongoUri.path.removePrefix("/").split("/")[1] + val collection = database.getCollection(collectionName) + fileNames.forEach { fileName -> + if (!fileName.startsWith("--")) { + println("Start process file $fileName") + val book = XSSFWorkbook(File(fileName).inputStream()) + book.use { xssfWorkbook -> + collectToList(xssfWorkbook.getSheetAt(0)).forEach {dict -> + if (!isEntryExistsInMongo(collection, dict)) { + collection.insertOne(dict) + } else { + val message = "--> Entry {id=${dict.id}, field=${dict.field}, value=${dict.value}, value_map=${dict.value_map}} is exist in collection '$collectionName' and was skipped" + println(message) + logger.warn { message } + } + } + } + } + } + } + println("Process is end. All entry was success added to mongo") + println("------") + } catch (e: Exception) { + println("Error process ${e.message}") + logger.error(e) { "Error process" } + } +} + +fun isEntryExistsInMongo(collection: MongoCollection, entry: Dict): Boolean { + return collection.findOne( + """ + { + "id": ${entry.id}, + "field": "${entry.field}", + "value": "${entry.value}", + "value_map": "${entry.value_map}" + } + """.trimIndent() + ) != null +} + +fun excelToJson(fileName: String) { + try { + if (!Files.exists(Paths.get(fileName))) { + val message = "File $fileName not found" + println(message) + logger.error { message } + return + } + val file = File(fileName) + val book = XSSFWorkbook(file.inputStream()) + book.use { + val lst = file.name.split(".") + val defaultFileName = "file-${LocalDateTime.now().format(DateTimeFormatter.ofPattern("ddMMyyyy-HHmm"))}.json" + val outFilename = "${if (file.toPath().parent != null) file.toPath().parent.toString() + FileSystems.getDefault().separator else ""}${if (lst.isNotEmpty()) file.name.split(".")[0] else defaultFileName}.json" + mapper.writeValue(File(outFilename), collectToList(it.getSheetAt(0))) + println("End process file. Write result to $outFilename") + println("-----------") + } + } catch (e: Exception) { + val message = "Error while process file $fileName" + println(message) + logger.error(e) { message } + } +} + +fun collectToList(sheet: XSSFSheet): List { + val list = ArrayList() + if (sheet.first().lastCellNum - sheet.first().firstCellNum < 4) { + throw RuntimeException("Column count < 4") + } + for (i in 1 until sheet.lastRowNum) { + val row = sheet.getRow(i) + if (row.getCell(0) == null) { + continue + } + val id = if (row.getCell(0).cellType == CellType.NUMERIC) { + row.getCell(0).numericCellValue.toInt() + } else { + row.getCell(0).stringCellValue.toInt() + } + val value = if (row.getCell(2).cellType == CellType.NUMERIC) { + row.getCell(2).numericCellValue.toInt().toString() + } else { + row.getCell(2).stringCellValue + } + val valueMap = if (row.getCell(3).cellType == CellType.NUMERIC) { + row.getCell(3).numericCellValue.toInt().toString() + } else { + row.getCell(3).stringCellValue + } + list.add(Dict( + id, + row.getCell(1).stringCellValue, + value, + valueMap, + )) + } + return UnmodifiableList(list) +} + +data class Dict (var id: Int, var field: String, var value: String, var value_map: String) diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 0000000..f85e9e6 --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=INFO, file +log4j.appender.file=org.apache.log4j.FileAppender +log4j.appender.file.layout=org.apache.log4j.PatternLayout +log4j.appender.file.layout.ConversionPattern=%5p [%t] (%F:%L) %-30.90c - %m%n +log4j.appender.file.file=${java.io.tmpdir}/exceltojson.log diff --git a/src/test/kotlin/ru/resprojects/exceltojson/MainTest.kt b/src/test/kotlin/ru/resprojects/exceltojson/MainTest.kt new file mode 100644 index 0000000..9aa8510 --- /dev/null +++ b/src/test/kotlin/ru/resprojects/exceltojson/MainTest.kt @@ -0,0 +1,28 @@ +package ru.resprojects.exceltojson + +import mu.KotlinLogging +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import org.junit.jupiter.api.Test +import java.io.File +import java.net.URL +import kotlin.test.assertEquals + + +data class DictVal (var id: Int, var field: String, var value: String, var value_map: String) + +private val logger = KotlinLogging.logger {} + +class MainTest { + + @Test + fun `when input correct xml file when expected not null list`() { + val book = XSSFWorkbook(File(loadResource("xlsx/test.xlsx").toURI()).inputStream()) + book.use { + assertEquals(2, collectToList(book.getSheetAt(0)).size) + } + } + + private fun loadResource(path: String): URL { + return Thread.currentThread().contextClassLoader.getResource(path) ?: throw RuntimeException("File $path not found") + } +} diff --git a/src/test/resources/xlsx/test.xlsx b/src/test/resources/xlsx/test.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7da08c5577eaad6ad68f80cc8dbd39ed06991993 GIT binary patch literal 9450 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 Cp>f#& literal 0 HcmV?d00001