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 0000000..7da08c5 Binary files /dev/null and b/src/test/resources/xlsx/test.xlsx differ