add google sheets
This commit is contained in:
parent
0769a2afad
commit
5e7ff8ca5a
|
|
@ -90,6 +90,9 @@ subprojects {
|
|||
dependency("software.amazon.awssdk:s3:2.10.11")
|
||||
|
||||
dependency("com.google.firebase:firebase-admin:9.0.0")
|
||||
dependency("com.google.api-client:google-api-client:1.33.0")
|
||||
dependency("com.google.apis:google-api-services-sheets:v4-rev20210629-1.32.1")
|
||||
dependency("com.google.auth:google-auth-library-oauth2-http:1.3.0")
|
||||
|
||||
dependency("com.github.ua-parser:uap-java:1.5.3")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,3 +62,5 @@ include("geoip-core")
|
|||
include("user-agent")
|
||||
include("smart-migration")
|
||||
include("telegram-bot-spring")
|
||||
include("spreadsheets")
|
||||
include("spreadsheets-google")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
plugins {
|
||||
id("kotlin")
|
||||
id("kotlin-spring")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":spreadsheets"))
|
||||
|
||||
implementation(project(":common"))
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
|
||||
implementation("org.springframework.boot:spring-boot")
|
||||
|
||||
implementation("com.google.api-client:google-api-client")
|
||||
implementation("com.google.apis:google-api-services-sheets")
|
||||
implementation("com.google.auth:google-auth-library-oauth2-http")
|
||||
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package ru.touchin.spreadsheets
|
||||
|
||||
import org.springframework.context.annotation.Import
|
||||
import ru.touchin.spreadsheets.google.configurations.GoogleSpreadSheetsConfiguration
|
||||
|
||||
@Suppress("unused")
|
||||
@Import(value = [GoogleSpreadSheetsConfiguration::class])
|
||||
annotation class EnableGoogleSpreadSheets
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package ru.touchin.spreadsheets.google.configurations
|
||||
|
||||
import com.google.api.client.http.javanet.NetHttpTransport
|
||||
import com.google.api.client.json.gson.GsonFactory
|
||||
import com.google.api.services.sheets.v4.Sheets
|
||||
import com.google.api.services.sheets.v4.SheetsScopes
|
||||
import com.google.auth.http.HttpCredentialsAdapter
|
||||
import com.google.auth.oauth2.GoogleCredentials
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import ru.touchin.spreadsheets.google.exceptions.GoogleCredentialsMissingException
|
||||
import ru.touchin.spreadsheets.google.properties.GoogleSpreadsheetsProperties
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
@Configuration
|
||||
@ComponentScan("ru.touchin.spreadsheets.google")
|
||||
@ConfigurationPropertiesScan("ru.touchin.spreadsheets.google")
|
||||
class GoogleSpreadSheetsConfiguration {
|
||||
|
||||
@Bean
|
||||
fun sheets(googleSheetsProperties: GoogleSpreadsheetsProperties): Sheets {
|
||||
val transport = NetHttpTransport.Builder().build()
|
||||
val jsonFactory = GsonFactory.getDefaultInstance()
|
||||
val initializer = HttpCredentialsAdapter(
|
||||
GoogleCredentials.fromStream(getCredentialsStream(googleSheetsProperties))
|
||||
.createScoped(SheetsScopes.SPREADSHEETS)
|
||||
.let { credentials ->
|
||||
googleSheetsProperties.delegate
|
||||
?.let { user ->
|
||||
credentials.createDelegated(user)
|
||||
}
|
||||
?: credentials
|
||||
}
|
||||
)
|
||||
|
||||
return Sheets.Builder(transport, jsonFactory, initializer)
|
||||
.setApplicationName(googleSheetsProperties.appName)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getCredentialsStream(googleSheetsProperties: GoogleSpreadsheetsProperties): InputStream {
|
||||
return googleSheetsProperties.credentialsPath
|
||||
?.takeIf(String::isNotBlank)
|
||||
?.let { File(it).inputStream() }
|
||||
?: googleSheetsProperties.credentials?.byteInputStream()
|
||||
?: throw GoogleCredentialsMissingException()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.spreadsheets.google.enums
|
||||
|
||||
enum class Dimension(val value: String) {
|
||||
Rows("ROWS"),
|
||||
Columns("COLUMNS");
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package ru.touchin.spreadsheets.google.enums
|
||||
|
||||
/**
|
||||
* @see <a href="https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption">Documentation</a>
|
||||
*/
|
||||
enum class ValueInputOption(val code: String) {
|
||||
|
||||
RAW("RAW"),
|
||||
USER_ENTERED("USER_ENTERED"),
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.spreadsheets.google.enums
|
||||
|
||||
/**
|
||||
* @see <a href="https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption">Documentation</a>
|
||||
*/
|
||||
enum class ValueRenderOption(val code: String) {
|
||||
|
||||
FORMATTED_VALUE("FORMATTED_VALUE"),
|
||||
UNFORMATTED_VALUE("UNFORMATTED_VALUE"),
|
||||
FORMULA("FORMULA"),
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.spreadsheets.google.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class GoogleCredentialsMissingException : CommonException("Google credentials are not set")
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package ru.touchin.spreadsheets.google.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
import ru.touchin.spreadsheets.services.dto.SpreadsheetSource
|
||||
|
||||
class GoogleSheetMissingException private constructor(
|
||||
val source: SpreadsheetSource,
|
||||
message: String,
|
||||
): CommonException(message) {
|
||||
|
||||
companion object {
|
||||
fun create(source: SpreadsheetSource) = GoogleSheetMissingException(
|
||||
source = source,
|
||||
message = "Missing sheet '${source.sheetId}' in spreadsheet '${source.spreadsheetId}",
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.spreadsheets.google.exceptions
|
||||
|
||||
import ru.touchin.common.exceptions.CommonException
|
||||
|
||||
class InvalidGoogleSpreadsheetsSourceException(
|
||||
url: String,
|
||||
) : CommonException("Unable to parse source $url")
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package ru.touchin.spreadsheets.google.properties
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.ConstructorBinding
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties(prefix = "spreadsheets.google")
|
||||
data class GoogleSpreadsheetsProperties(
|
||||
val appName: String,
|
||||
val credentials: String? = null,
|
||||
val credentialsPath: String? = null,
|
||||
val delegate: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package ru.touchin.spreadsheets.google.services
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import ru.touchin.spreadsheets.google.exceptions.InvalidGoogleSpreadsheetsSourceException
|
||||
import ru.touchin.spreadsheets.services.SpreadsheetsUrlService
|
||||
import ru.touchin.spreadsheets.services.dto.SpreadsheetSource
|
||||
import java.net.URL
|
||||
|
||||
@Service
|
||||
class GoogleSheetsUrlServiceImpl : SpreadsheetsUrlService {
|
||||
|
||||
override fun parse(url: URL): SpreadsheetSource {
|
||||
return parse(url.toString())
|
||||
}
|
||||
|
||||
override fun parse(url: String): SpreadsheetSource {
|
||||
val groups = spreadSheetUrlRegex.find(url)?.groups
|
||||
?: throw InvalidGoogleSpreadsheetsSourceException(url)
|
||||
|
||||
val spreadSheetId = groups["spreadsheetId"]?.value
|
||||
?: throw InvalidGoogleSpreadsheetsSourceException(url)
|
||||
|
||||
val sheetId = groups["sheetId"]?.value?.toInt()
|
||||
?: 0
|
||||
|
||||
return SpreadsheetSource(spreadsheetId = spreadSheetId, sheetId = sheetId)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val spreadSheetUrlRegex = Regex(
|
||||
"https://docs\\.google\\.com/spreadsheets/d/(?<spreadsheetId>[\\da-zA-Z-_]+)/edit(?:#gid=(?<sheetId>\\d+))?",
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package ru.touchin.spreadsheets.google.services
|
||||
|
||||
import org.springframework.beans.factory.ObjectFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import ru.touchin.spreadsheets.services.SpreadsheetBuilder
|
||||
import ru.touchin.spreadsheets.services.SpreadsheetBuilderFactory
|
||||
|
||||
@Service
|
||||
class GoogleSpreadsheetBuilderFactoryImpl(
|
||||
private val spreadsheetBuilderFactory: ObjectFactory<SpreadsheetBuilder>,
|
||||
) : SpreadsheetBuilderFactory {
|
||||
|
||||
override fun create(): SpreadsheetBuilder {
|
||||
return spreadsheetBuilderFactory.`object`
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package ru.touchin.spreadsheets.google.services
|
||||
|
||||
import com.google.api.services.sheets.v4.Sheets
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.spreadsheets.services.SpreadsheetBuilder
|
||||
import ru.touchin.spreadsheets.services.SpreadsheetService
|
||||
import ru.touchin.spreadsheets.services.SpreadsheetsUrlService
|
||||
import java.net.URL
|
||||
|
||||
@Component
|
||||
class GoogleSpreadsheetBuilderImpl(
|
||||
private val spreadsheetsUrlService: SpreadsheetsUrlService,
|
||||
private val sheets: Sheets,
|
||||
) : SpreadsheetBuilder {
|
||||
|
||||
private var sheetId: Int? = null
|
||||
|
||||
private var autoInit: Boolean = true
|
||||
|
||||
override fun setSheetId(sheetId: Int): SpreadsheetBuilder = this.also {
|
||||
it.sheetId = sheetId
|
||||
}
|
||||
|
||||
override fun setAutoInit(autoInit: Boolean): SpreadsheetBuilder = this.also {
|
||||
it.autoInit = autoInit
|
||||
}
|
||||
|
||||
override fun build(url: URL): SpreadsheetService {
|
||||
val source = spreadsheetsUrlService.parse(url)
|
||||
val sheetId = this.sheetId ?: source.sheetId
|
||||
|
||||
return GoogleSpreadsheetServiceImpl(source.copy(sheetId = sheetId), sheets)
|
||||
.also {
|
||||
if (autoInit) {
|
||||
it.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
package ru.touchin.spreadsheets.google.services
|
||||
|
||||
import com.google.api.services.sheets.v4.Sheets
|
||||
import com.google.api.services.sheets.v4.model.BatchGetValuesByDataFilterRequest
|
||||
import com.google.api.services.sheets.v4.model.BatchUpdateSpreadsheetRequest
|
||||
import com.google.api.services.sheets.v4.model.CellData
|
||||
import com.google.api.services.sheets.v4.model.DataFilter
|
||||
import com.google.api.services.sheets.v4.model.DimensionRange
|
||||
import com.google.api.services.sheets.v4.model.ExtendedValue
|
||||
import com.google.api.services.sheets.v4.model.GridCoordinate
|
||||
import com.google.api.services.sheets.v4.model.GridRange
|
||||
import com.google.api.services.sheets.v4.model.InsertDimensionRequest
|
||||
import com.google.api.services.sheets.v4.model.Request
|
||||
import com.google.api.services.sheets.v4.model.RowData
|
||||
import com.google.api.services.sheets.v4.model.UpdateCellsRequest
|
||||
import ru.touchin.spreadsheets.google.enums.Dimension
|
||||
import ru.touchin.spreadsheets.google.enums.ValueRenderOption
|
||||
import ru.touchin.spreadsheets.google.exceptions.GoogleSheetMissingException
|
||||
import ru.touchin.spreadsheets.services.SpreadsheetService
|
||||
import ru.touchin.spreadsheets.services.dto.CellType
|
||||
import ru.touchin.spreadsheets.services.dto.SheetCell
|
||||
import ru.touchin.spreadsheets.services.dto.SheetHeader
|
||||
import ru.touchin.spreadsheets.services.dto.SheetRange
|
||||
import ru.touchin.spreadsheets.services.dto.Spreadsheet
|
||||
import ru.touchin.spreadsheets.services.dto.SpreadsheetSource
|
||||
import ru.touchin.spreadsheets.services.dto.cells.SheetStringCell
|
||||
import java.math.BigDecimal
|
||||
import kotlin.math.max
|
||||
|
||||
internal class GoogleSpreadsheetServiceImpl(
|
||||
private val source: SpreadsheetSource,
|
||||
private val sheets: Sheets,
|
||||
) : SpreadsheetService {
|
||||
|
||||
private lateinit var spreadsheet: Spreadsheet
|
||||
|
||||
override fun init(): Spreadsheet {
|
||||
val spreadsheet = sheets.spreadsheets()
|
||||
.get(source.spreadsheetId)
|
||||
.execute()
|
||||
|
||||
val sheet = spreadsheet.sheets
|
||||
.find { it.properties.sheetId == source.sheetId }
|
||||
?: throw GoogleSheetMissingException.create(source)
|
||||
|
||||
val gridProperties = sheet.properties.gridProperties
|
||||
|
||||
return Spreadsheet(
|
||||
rowCount = gridProperties.rowCount,
|
||||
columnCount = gridProperties.columnCount,
|
||||
locale = spreadsheet.properties.locale,
|
||||
).also {
|
||||
this.spreadsheet = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSheet(): Spreadsheet {
|
||||
return spreadsheet.copy()
|
||||
}
|
||||
|
||||
override fun getHeader(): SheetHeader {
|
||||
val values = getValues(source.headerRange)
|
||||
.map { rows ->
|
||||
rows.map { SheetStringCell(it.toString()) }
|
||||
}
|
||||
|
||||
return SheetHeader(values)
|
||||
}
|
||||
|
||||
override fun createHeader(header: SheetHeader) {
|
||||
updateValues(source.headerRange, header.values)
|
||||
}
|
||||
|
||||
override fun getContentColumn(column: Int): List<Any> {
|
||||
return getValues(
|
||||
getContentRange().copy(
|
||||
startColumnIndex = column,
|
||||
endColumnIndex = column + 1,
|
||||
),
|
||||
dimension = Dimension.Columns,
|
||||
)
|
||||
.firstOrNull()
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
override fun updateRows(range: SheetRange, values: List<List<SheetCell>>) {
|
||||
return updateValues(range, values)
|
||||
}
|
||||
|
||||
override fun updateContentRows(startRowIndex: Int, values: List<List<SheetCell>>) {
|
||||
updateRows(
|
||||
range = SheetRange(
|
||||
startColumnIndex = 0,
|
||||
startRowIndex = startRowIndex + (source.headerRange.endRowIndex ?: 0),
|
||||
),
|
||||
values = values,
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateValues(range: SheetRange, values: List<List<SheetCell>>) {
|
||||
val maxColumnIndex = getMaxIndex(values, Dimension.Columns) + range.startColumnIndex
|
||||
val maxRowIndex = getMaxIndex(values, Dimension.Rows) + range.startRowIndex
|
||||
|
||||
val requests = mutableListOf<Request>()
|
||||
|
||||
if (maxColumnIndex > spreadsheet.columnCount) {
|
||||
requests.add(
|
||||
createInsertRequest(
|
||||
insertCount = maxColumnIndex - spreadsheet.columnCount,
|
||||
dimension = Dimension.Columns,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (maxRowIndex > spreadsheet.rowCount) {
|
||||
requests.add(
|
||||
createInsertRequest(
|
||||
insertCount = maxRowIndex - spreadsheet.rowCount,
|
||||
dimension = Dimension.Rows,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
requests.add(
|
||||
createUpdateCellsRequest(range, values)
|
||||
)
|
||||
|
||||
sheets.spreadsheets()
|
||||
.batchUpdate(
|
||||
source.spreadsheetId,
|
||||
BatchUpdateSpreadsheetRequest().apply {
|
||||
this.requests = requests
|
||||
}
|
||||
)
|
||||
.execute()
|
||||
|
||||
spreadsheet = spreadsheet.copy(
|
||||
columnCount = max(spreadsheet.columnCount, maxColumnIndex),
|
||||
rowCount = max(spreadsheet.rowCount, maxRowIndex),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createUpdateCellsRequest(range: SheetRange, values: List<List<SheetCell>>): Request {
|
||||
return Request().setUpdateCells(
|
||||
UpdateCellsRequest().apply {
|
||||
fields = "*"
|
||||
start = GridCoordinate().apply {
|
||||
sheetId = source.sheetId
|
||||
columnIndex = range.startColumnIndex
|
||||
rowIndex = range.startRowIndex
|
||||
}
|
||||
rows = values.map { rows ->
|
||||
RowData().setValues(
|
||||
rows.map { row ->
|
||||
row.toCellData()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getContentRange(): SheetRange {
|
||||
return SheetRange(
|
||||
startColumnIndex = 0,
|
||||
startRowIndex = source.headerRange.endRowIndex ?: 0,
|
||||
)
|
||||
}
|
||||
|
||||
private fun SheetCell.toCellData(): CellData {
|
||||
val value = ExtendedValue()
|
||||
|
||||
when(type) {
|
||||
CellType.String -> value.stringValue = this.value.toString()
|
||||
CellType.Null -> value.stringValue = null
|
||||
CellType.Date -> value.stringValue = this.value.toString()
|
||||
CellType.DateTime -> value.stringValue = this.value.toString()
|
||||
CellType.Price -> value.numberValue = (this.value as BigDecimal).toDouble()
|
||||
CellType.Number -> value.numberValue = (this.value as Number).toDouble()
|
||||
}
|
||||
|
||||
return CellData().also { it.userEnteredValue = value }
|
||||
}
|
||||
|
||||
private fun createInsertRequest(insertCount: Int, dimension: Dimension): Request {
|
||||
val startIndex = when(dimension) {
|
||||
Dimension.Rows -> spreadsheet.rowCount - 1
|
||||
Dimension.Columns -> spreadsheet.columnCount - 1
|
||||
}
|
||||
|
||||
return Request().setInsertDimension(
|
||||
InsertDimensionRequest()
|
||||
.setRange(
|
||||
DimensionRange()
|
||||
.setSheetId(source.sheetId)
|
||||
.setDimension(dimension.value)
|
||||
.setStartIndex(startIndex)
|
||||
.setEndIndex(startIndex + insertCount)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMaxIndex(values: List<List<SheetCell>>, dimension: Dimension): Int {
|
||||
return when (dimension) {
|
||||
Dimension.Rows -> values.size
|
||||
Dimension.Columns -> values.maxOf { it.size }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getValues(range: SheetRange, dimension: Dimension = Dimension.Rows): List<List<Any>> {
|
||||
val values = sheets.spreadsheets()
|
||||
.values()
|
||||
.batchGetByDataFilter(
|
||||
source.spreadsheetId,
|
||||
BatchGetValuesByDataFilterRequest().apply {
|
||||
majorDimension = dimension.value
|
||||
valueRenderOption = ValueRenderOption.FORMATTED_VALUE.code
|
||||
dataFilters = listOf(
|
||||
DataFilter().apply {
|
||||
gridRange = range.toGridRange()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
.execute()
|
||||
.valueRanges
|
||||
.first()
|
||||
.valueRange
|
||||
.getValues()
|
||||
|
||||
return values?.toList() ?: emptyList()
|
||||
}
|
||||
|
||||
private fun SheetRange.toGridRange(): GridRange {
|
||||
return GridRange().also {
|
||||
it.sheetId = source.sheetId
|
||||
it.startColumnIndex = startColumnIndex
|
||||
it.startRowIndex = startRowIndex
|
||||
it.endColumnIndex = endColumnIndex
|
||||
it.endRowIndex = endRowIndex
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package ru.touchin.spreadsheets.google.utils
|
||||
|
||||
import java.net.URL
|
||||
|
||||
object GoogleSpreadSheetsFormulaParserUtil {
|
||||
|
||||
private const val HYPERLINK_REGEX_LINK_INDEX: Int = 1
|
||||
private const val RUS_LOCALE = "ru_RU"
|
||||
private const val DEFAULT_DIVIDER = ","
|
||||
private const val RUS_DIVIDER = ";"
|
||||
|
||||
fun isFormulaHyperlink(jiraIssueLink: String, locale: String): Boolean {
|
||||
return getHyperLinkRegexByLocale(locale).matches(jiraIssueLink)
|
||||
}
|
||||
|
||||
fun asFormulaHyperlink(url: URL, label: String, locale: String): String {
|
||||
val divider = getFormulaeDividerByLocale(locale)
|
||||
return "=HYPERLINK(\"$url\"$divider\"$label\")"
|
||||
}
|
||||
|
||||
fun fromFormulaHyperlink(hyperlink: String, locale: String): URL {
|
||||
return getHyperLinkRegexByLocale(locale).matchEntire(hyperlink)
|
||||
?.groupValues
|
||||
?.get(HYPERLINK_REGEX_LINK_INDEX)
|
||||
?.trim { it == '"' }
|
||||
?.let { URL(it) }
|
||||
?: throw IllegalStateException("Could not parse hyperlink \"$hyperlink\"")
|
||||
}
|
||||
|
||||
private fun getHyperLinkRegexByLocale(locale: String): Regex {
|
||||
val divider = getFormulaeDividerByLocale(locale)
|
||||
return Regex("=HYPERLINK\\((.*)$divider(.*)\\)")
|
||||
}
|
||||
|
||||
private fun getFormulaeDividerByLocale(locale: String): String {
|
||||
return if (locale == RUS_LOCALE) {
|
||||
RUS_DIVIDER
|
||||
} else {
|
||||
DEFAULT_DIVIDER
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.sheets.google
|
||||
|
||||
import org.springframework.boot.SpringBootConfiguration
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
|
||||
@ComponentScan
|
||||
@TestConfiguration
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
class GoogleSheetsTestApplication
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package ru.touchin.sheets.google.services
|
||||
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import ru.touchin.spreadsheets.google.exceptions.InvalidGoogleSpreadsheetsSourceException
|
||||
import ru.touchin.spreadsheets.google.services.GoogleSheetsUrlServiceImpl
|
||||
import ru.touchin.spreadsheets.services.SpreadsheetsUrlService
|
||||
import ru.touchin.spreadsheets.services.dto.SpreadsheetSource
|
||||
|
||||
@SpringBootTest(
|
||||
classes = [
|
||||
GoogleSheetsUrlServiceImpl::class,
|
||||
]
|
||||
)
|
||||
internal class GoogleSheetsUrlServiceImplTest {
|
||||
|
||||
@Autowired
|
||||
lateinit var sheetsUrlService: SpreadsheetsUrlService
|
||||
|
||||
@Test
|
||||
fun shouldParseOk() {
|
||||
val expectedSpreadsheetSource = SpreadsheetSource(
|
||||
spreadsheetId = "1KgQTBmFq-yddGotA5PUd5SRkg-J3dtdULpRNz7uctAE",
|
||||
sheetId = 1,
|
||||
)
|
||||
|
||||
val url = "https://docs.google.com/spreadsheets/d/${expectedSpreadsheetSource.spreadsheetId}/edit#gid=${expectedSpreadsheetSource.sheetId}"
|
||||
|
||||
val text1 = """
|
||||
Test
|
||||
Test $url
|
||||
ssss
|
||||
""".trimIndent()
|
||||
|
||||
val actualSpreadsheetSource1 = sheetsUrlService.parse(text1)
|
||||
|
||||
assertEquals(expectedSpreadsheetSource, actualSpreadsheetSource1)
|
||||
|
||||
val text2 = """
|
||||
Test ($url)
|
||||
""".trimIndent()
|
||||
|
||||
val actualSpreadsheetSource2 = sheetsUrlService.parse(text2)
|
||||
|
||||
assertEquals(expectedSpreadsheetSource, actualSpreadsheetSource2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldParseOk2() {
|
||||
val expectedSpreadsheetSource = SpreadsheetSource(
|
||||
spreadsheetId = "1KgQTBmFq-yddGotA5PUd5SRkg-J3dtdULpRNz7uctAE",
|
||||
sheetId = 0,
|
||||
)
|
||||
|
||||
val text = """
|
||||
Test
|
||||
Test https://docs.google.com/spreadsheets/d/${expectedSpreadsheetSource.spreadsheetId}/edit Test
|
||||
Test
|
||||
""".trimIndent()
|
||||
|
||||
val actualSpreadsheetSource1 = sheetsUrlService.parse(text)
|
||||
|
||||
assertEquals(expectedSpreadsheetSource, actualSpreadsheetSource1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldThrowException() {
|
||||
val text1 = """
|
||||
Test
|
||||
""".trimIndent()
|
||||
|
||||
assertThrows(InvalidGoogleSpreadsheetsSourceException::class.java) {
|
||||
sheetsUrlService.parse(text1)
|
||||
}
|
||||
|
||||
val text2 = """
|
||||
Test (https://dcs.google.com/spreadsheets/d/1KgQTBmFq-yddGotA5PUd5SRkg-J3dtdULpRNz7uctAE/edit#gid=0)
|
||||
""".trimIndent()
|
||||
|
||||
assertThrows(InvalidGoogleSpreadsheetsSourceException::class.java) {
|
||||
sheetsUrlService.parse(text2)
|
||||
}
|
||||
|
||||
val text3 = """
|
||||
Test (https://dcs.google.com/spreadsheets/d/1KgQTBmFq-yddGotA5PUd5SRkg-J
|
||||
3dtdULpRNz7uctAE/edit#gid=0)
|
||||
""".trimIndent()
|
||||
|
||||
assertThrows(InvalidGoogleSpreadsheetsSourceException::class.java) {
|
||||
sheetsUrlService.parse(text3)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package ru.touchin.sheets.google.utils
|
||||
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import ru.touchin.spreadsheets.google.utils.GoogleSpreadSheetsFormulaParserUtil
|
||||
import java.net.URL
|
||||
|
||||
class GoogleSheetsFormulaParserUtilTest {
|
||||
|
||||
private val url = URL("https://example.com")
|
||||
|
||||
@Test
|
||||
fun isFormulaHyperlink_trueIfCorrectSyntax() {
|
||||
val text = "=HYPERLINK(someurl;label)"
|
||||
|
||||
val result = GoogleSpreadSheetsFormulaParserUtil.isFormulaHyperlink(text, "ru_RU")
|
||||
|
||||
Assertions.assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isFormulaHyperlink_falseIfUrl() {
|
||||
val result = GoogleSpreadSheetsFormulaParserUtil.isFormulaHyperlink(url.toString(), "ru_RU")
|
||||
|
||||
Assertions.assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun asFormulaHyperlink_correct() {
|
||||
val label = "link"
|
||||
|
||||
val expected = "=HYPERLINK(\"$url\";\"$label\")"
|
||||
val actual = GoogleSpreadSheetsFormulaParserUtil.asFormulaHyperlink(url, label, "ru_RU")
|
||||
|
||||
Assertions.assertEquals(
|
||||
expected,
|
||||
actual
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fromFormulaHyperlink_correct() {
|
||||
val expected = url.toString()
|
||||
val actual = GoogleSpreadSheetsFormulaParserUtil.fromFormulaHyperlink("=HYPERLINK(\"$url\";\"label\")", "ru_RU").toString()
|
||||
|
||||
|
||||
Assertions.assertEquals(
|
||||
expected,
|
||||
actual
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
plugins {
|
||||
id("kotlin")
|
||||
id("kotlin-spring")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.spreadsheets.services
|
||||
|
||||
import java.net.URL
|
||||
|
||||
interface SpreadsheetBuilder {
|
||||
|
||||
fun setSheetId(sheetId: Int): SpreadsheetBuilder
|
||||
fun setAutoInit(autoInit: Boolean): SpreadsheetBuilder
|
||||
|
||||
fun build(url: URL): SpreadsheetService
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.spreadsheets.services
|
||||
|
||||
interface SpreadsheetBuilderFactory {
|
||||
|
||||
fun create(): SpreadsheetBuilder
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package ru.touchin.spreadsheets.services
|
||||
|
||||
import ru.touchin.spreadsheets.services.dto.SheetCell
|
||||
import ru.touchin.spreadsheets.services.dto.SheetHeader
|
||||
import ru.touchin.spreadsheets.services.dto.SheetRange
|
||||
import ru.touchin.spreadsheets.services.dto.Spreadsheet
|
||||
|
||||
interface SpreadsheetService {
|
||||
|
||||
fun init(): Spreadsheet
|
||||
|
||||
fun getSheet(): Spreadsheet
|
||||
|
||||
fun getHeader(): SheetHeader
|
||||
|
||||
fun createHeader(header: SheetHeader)
|
||||
|
||||
fun getContentColumn(column: Int): List<Any>
|
||||
|
||||
fun updateRows(range: SheetRange, values: List<List<SheetCell>>)
|
||||
|
||||
fun updateContentRows(startRowIndex: Int, values: List<List<SheetCell>>)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.spreadsheets.services
|
||||
|
||||
import ru.touchin.spreadsheets.services.dto.SpreadsheetSource
|
||||
import java.net.URL
|
||||
|
||||
interface SpreadsheetsUrlService {
|
||||
|
||||
fun parse(url: URL): SpreadsheetSource
|
||||
|
||||
fun parse(url: String): SpreadsheetSource
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ru.touchin.spreadsheets.services.dto
|
||||
|
||||
enum class CellType {
|
||||
Null,
|
||||
String,
|
||||
Date,
|
||||
DateTime,
|
||||
Price,
|
||||
Number,
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.spreadsheets.services.dto
|
||||
|
||||
interface SheetCell {
|
||||
val value: Any
|
||||
val type: CellType
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.spreadsheets.services.dto
|
||||
|
||||
data class SheetHeader(
|
||||
val values: List<List<SheetCell>>
|
||||
) {
|
||||
|
||||
fun hasHeader() = values.isNotEmpty()
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package ru.touchin.spreadsheets.services.dto
|
||||
|
||||
data class SheetRange(
|
||||
val startColumnIndex: Int,
|
||||
val startRowIndex: Int,
|
||||
val endColumnIndex: Int? = null,
|
||||
val endRowIndex: Int? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.spreadsheets.services.dto
|
||||
|
||||
data class Spreadsheet(
|
||||
val rowCount: Int,
|
||||
val columnCount: Int,
|
||||
val locale: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package ru.touchin.spreadsheets.services.dto
|
||||
|
||||
data class SpreadsheetSource(
|
||||
val spreadsheetId: String,
|
||||
val sheetId: Int,
|
||||
val headerRange: SheetRange = DEFAULT_HEADER_RANGE,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val DEFAULT_HEADER_RANGE = SheetRange(
|
||||
startColumnIndex = 0,
|
||||
startRowIndex = 0,
|
||||
endRowIndex = 1,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package ru.touchin.spreadsheets.services.dto.cells
|
||||
|
||||
import ru.touchin.spreadsheets.services.dto.CellType
|
||||
import ru.touchin.spreadsheets.services.dto.SheetCell
|
||||
|
||||
class SheetNullCell() : SheetCell {
|
||||
|
||||
override val value = 0
|
||||
|
||||
override val type = CellType.Null
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SheetNullCell
|
||||
|
||||
if (value != other.value) return false
|
||||
if (type != other.type) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = value
|
||||
result = 31 * result + type.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ru.touchin.spreadsheets.services.dto.cells
|
||||
|
||||
import ru.touchin.spreadsheets.services.dto.CellType
|
||||
import ru.touchin.spreadsheets.services.dto.SheetCell
|
||||
|
||||
data class SheetNumberCell(override val value: Number) : SheetCell {
|
||||
|
||||
override val type = CellType.Number
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ru.touchin.spreadsheets.services.dto.cells
|
||||
|
||||
import ru.touchin.spreadsheets.services.dto.CellType
|
||||
import ru.touchin.spreadsheets.services.dto.SheetCell
|
||||
|
||||
data class SheetStringCell(override val value: String) : SheetCell {
|
||||
|
||||
override val type = CellType.String
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue