From 064885d0895e6b61cf07af97f5836c513591121f Mon Sep 17 00:00:00 2001 From: Hieu Duong Date: Thu, 24 Sep 2020 13:25:38 +0700 Subject: [PATCH 01/38] Add CODEOWNER --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..7876f5a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @treasure-data/integrations From cbd1065ba7c6bafa33da8c3ccc8738560a2f5499 Mon Sep 17 00:00:00 2001 From: huylenq Date: Tue, 10 Nov 2020 13:15:29 +0700 Subject: [PATCH 02/38] Apply Embulk Gradle plugin and upgrade Gradle wrapper --- build.gradle | 85 ++++++------------ .../embulkPluginRuntime.lockfile | 14 +++ gradle/wrapper/gradle-wrapper.jar | Bin 54708 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 18 +++- gradlew.bat | 18 +++- 6 files changed, 75 insertions(+), 62 deletions(-) create mode 100644 gradle/dependency-locks/embulkPluginRuntime.lockfile diff --git a/build.gradle b/build.gradle index 4495645..d9f6764 100644 --- a/build.gradle +++ b/build.gradle @@ -1,43 +1,37 @@ plugins { - id "com.jfrog.bintray" version "1.1" - id "com.github.jruby-gradle.base" version "1.5.0" id "java" id "checkstyle" id "jacoco" + id "maven" + id 'maven-publish' + id "org.embulk.embulk-plugins" version "0.4.2" } -import com.github.jrubygradle.JRubyExec repositories { mavenCentral() jcenter() } -configurations { - provided -} -version = "0.2.10" +version = "0.2.11.SNAPSHOT" +group = "com.treasuredata.embulk.plugins" +description = "JIRA Embulk input plugin." sourceCompatibility = 1.8 targetCompatibility = 1.8 +def embulkVersion = '0.9.20' + dependencies { - compile "org.embulk:embulk-core:0.9.20" - provided "org.embulk:embulk-core:0.9.20" + compileOnly "org.embulk:embulk-core:$embulkVersion" + compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.6' compile group: 'org.glassfish.jersey.core', name: 'jersey-client', version: '2.27' compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5' testCompile "junit:junit:4.+" testCompile "org.mockito:mockito-core:2.+" - testCompile "org.embulk:embulk-test:0.9.20" - testCompile "org.embulk:embulk-core:0.9.20:tests" + testCompile "org.embulk:embulk-test:$embulkVersion" + testCompile "org.embulk:embulk-core:$embulkVersion:tests" } -task classpath(type: Copy, dependsOn: ["jar"]) { - doFirst { file("classpath").deleteDir() } - from (configurations.runtime - configurations.provided + files(jar.archivePath)) - into "classpath" -} -clean { delete "classpath" } - checkstyle { configFile = file("${project.rootDir}/config/checkstyle/checkstyle.xml") toolVersion = '6.14.1' @@ -55,24 +49,11 @@ task checkstyle(type: Checkstyle) { source = sourceSets.main.allJava + sourceSets.test.allJava } -task gem(type: JRubyExec, dependsOn: ["gemspec", "classpath"]) { - jrubyArgs "-S" - script "gem" - scriptArgs "build", "${project.name}.gemspec" - doLast { ant.move(file: "${project.name}-${project.version}.gem", todir: "pkg") } -} -task gemPush(type: JRubyExec, dependsOn: ["gem"]) { - jrubyArgs "-S" - script "gem" - scriptArgs "push", "pkg/${project.name}-${project.version}.gem" -} - -task "package"(dependsOn: ["gemspec", "classpath"]) { - doLast { - println "> Build succeeded." - println "> You can run embulk with '-L ${file(".").absolutePath}' argument." - } +embulkPlugin { + mainClass = "org.embulk.input.jira.JiraInputPlugin" + category = "input" + type = "jira" } jacocoTestReport { @@ -80,33 +61,19 @@ jacocoTestReport { reports { xml.enabled false csv.enabled false - html.destination "${buildDir}/reports/coverage" + html.destination file("${buildDir}/reports/coverage") } } -task gemspec { - ext.gemspecFile = file("${project.name}.gemspec") - inputs.file "build.gradle" - outputs.file gemspecFile - doLast { gemspecFile.write($/ -Gem::Specification.new do |spec| - spec.name = "${project.name}" - spec.version = "${project.version}" - spec.authors = ["uu59", "yoshihara"] - spec.summary = %[Jira input plugin for Embulk] - spec.description = %[Loads records from Jira.] - spec.email = ["k@uu59.org", "h.yoshihara@everyleaf.com"] - spec.licenses = ["Apache-2.0"] - spec.homepage = "https://github.com/treasure-data/embulk-input-jira" - spec.files = `git ls-files`.split("\n") + Dir["classpath/*.jar"] - spec.test_files = spec.files.grep(%r"^(test|spec)/") - spec.require_paths = ["lib"] +gem { + authors = ['uu59', 'yoshihara'] + email = ['k@uu59.org', 'h.yoshihara@everyleaf.com'] + summary = project.description + homepage = 'https://github.com/treasure-data/embulk-input-jira' + licenses = ['Apache-2.0'] +} - spec.add_development_dependency 'bundler', ['~> 1.0'] - spec.add_development_dependency 'rake', ['~> 12.0'] -end -/$) - } +gemPush { + host = "https://treasuredata.jfrog.io/treasuredata/api/gems/rubygems-local" } -clean { delete "${project.name}.gemspec" } diff --git a/gradle/dependency-locks/embulkPluginRuntime.lockfile b/gradle/dependency-locks/embulkPluginRuntime.lockfile new file mode 100644 index 0000000..c252ca7 --- /dev/null +++ b/gradle/dependency-locks/embulkPluginRuntime.lockfile @@ -0,0 +1,14 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.google.code.gson:gson:2.8.5 +commons-codec:commons-codec:1.10 +commons-logging:commons-logging:1.2 +javax.annotation:javax.annotation-api:1.2 +javax.ws.rs:javax.ws.rs-api:2.1 +org.apache.httpcomponents:httpclient:4.5.6 +org.apache.httpcomponents:httpcore:4.4.10 +org.glassfish.hk2.external:javax.inject:2.5.0-b42 +org.glassfish.hk2:osgi-resource-locator:1.0.1 +org.glassfish.jersey.core:jersey-client:2.27 +org.glassfish.jersey.core:jersey-common:2.27 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7a3265ee94c0ab25cf079ac8ccdf87f41d455d42..5c2d1cf016b3885f6930543d57b744ea8c220a1a 100644 GIT binary patch literal 55616 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

_x$+l{{cH8$W#CT diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f16d266..bb8b2fc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-bin.zip diff --git a/gradlew b/gradlew index cccdd3d..8e25e6c 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/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 @@ -28,7 +44,7 @@ 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="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/gradlew.bat b/gradlew.bat index e95643d..24467a1 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@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 @@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome From c3b19f6c871afb881ddf8e98924890c4f602171f Mon Sep 17 00:00:00 2001 From: huylenq Date: Fri, 13 Nov 2020 11:40:08 +0700 Subject: [PATCH 03/38] Correct the Ruby gem host --- build.gradle | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index d9f6764..8a9111e 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,6 @@ task checkstyle(type: Checkstyle) { source = sourceSets.main.allJava + sourceSets.test.allJava } - embulkPlugin { mainClass = "org.embulk.input.jira.JiraInputPlugin" category = "input" @@ -65,7 +64,6 @@ jacocoTestReport { } } - gem { authors = ['uu59', 'yoshihara'] email = ['k@uu59.org', 'h.yoshihara@everyleaf.com'] @@ -75,5 +73,5 @@ gem { } gemPush { - host = "https://treasuredata.jfrog.io/treasuredata/api/gems/rubygems-local" -} + host = "https://rubygems.org" +} \ No newline at end of file From cc056da97b8444db6986b9a2cfb075e9459480e5 Mon Sep 17 00:00:00 2001 From: huylenq Date: Fri, 13 Nov 2020 11:40:32 +0700 Subject: [PATCH 04/38] Fix: including guess plugin --- build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build.gradle b/build.gradle index 8a9111e..6fbcbb1 100644 --- a/build.gradle +++ b/build.gradle @@ -70,6 +70,14 @@ gem { summary = project.description homepage = 'https://github.com/treasure-data/embulk-input-jira' licenses = ['Apache-2.0'] + + generateRubyCode = false + into("lib/embulk/input/") { + from "lib/embulk/input/jira.rb" + } + into("lib/embulk/guess/") { + from "lib/embulk/guess/jira.rb" + } } gemPush { From 24ede9c4f9d6c65bc81c6a1c517a4b52dbf74abe Mon Sep 17 00:00:00 2001 From: huylenq Date: Thu, 12 Nov 2020 13:07:13 +0700 Subject: [PATCH 05/38] Upgrade Embulk to v0.10.19, along with necessary refactors --- build.gradle | 38 +++++++- .../embulkPluginRuntime.lockfile | 11 +++ .../embulk/input/jira/JiraInputPlugin.java | 74 +++++++++++---- .../embulk/input/jira/client/JiraClient.java | 19 ++-- .../org/embulk/input/jira/util/JiraUtil.java | 20 ++-- .../input/jira/JiraInputPluginTest.java | 58 ++++++------ .../input/jira/JiraPluginTestRuntime.java | 92 ++++++------------- .../org/embulk/input/jira/TestHelpers.java | 49 ++++++---- .../input/jira/client/JiraClientTest.java | 7 +- .../embulk/input/jira/util/JiraUtilTest.java | 44 +++++---- src/test/resources/config.yml | 13 --- 11 files changed, 245 insertions(+), 180 deletions(-) delete mode 100644 src/test/resources/config.yml diff --git a/build.gradle b/build.gradle index 6fbcbb1..5b9fca6 100644 --- a/build.gradle +++ b/build.gradle @@ -11,25 +11,57 @@ repositories { jcenter() } -version = "0.2.11.SNAPSHOT" +version = "0.2.11.alpha.0" group = "com.treasuredata.embulk.plugins" description = "JIRA Embulk input plugin." sourceCompatibility = 1.8 targetCompatibility = 1.8 -def embulkVersion = '0.9.20' +def embulkVersion = '0.10.19' dependencies { + compileOnly "org.embulk:embulk-api:$embulkVersion" + compileOnly "org.embulk:embulk-spi:$embulkVersion" + + // Required through GuessExecutor usage. compileOnly "org.embulk:embulk-core:$embulkVersion" + compile('org.embulk:embulk-util-config:0.1.4') { + // Conflict with Embulk Core + exclude group: 'com.fasterxml.jackson.core', module: 'jackson-annotations' + exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' + exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind' + exclude group: 'com.fasterxml.jackson.datatype', module: 'jackson-datatype-jdk8' + exclude group: 'javax.validation', module: 'validation-api' + } + compile('org.embulk:embulk-util-retryhelper:0.8.1') { + exclude group: 'org.slf4j', module: 'slf4j-api' + } + compile 'org.embulk:embulk-util-timestamp:0.2.1' + + compile('org.embulk:embulk-util-json:0.1.0') { + exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' + exclude group: 'org.msgpack', module: 'msgpack-core' + } + + // Explicit dependencies for embulk-util-* that matches with Embulk 0.10.19 + compile 'com.fasterxml.jackson.core:jackson-core:2.6.7' + compile 'com.fasterxml.jackson.core:jackson-annotations:2.6.7' + compile 'com.fasterxml.jackson.core:jackson-databind:2.6.7' + compile 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.6.7' + compile 'javax.validation:validation-api:1.1.0.Final' + compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.6' compile group: 'org.glassfish.jersey.core', name: 'jersey-client', version: '2.27' compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5' + compile 'com.google.guava:guava:18.0' + testCompile "junit:junit:4.+" testCompile "org.mockito:mockito-core:2.+" - testCompile "org.embulk:embulk-test:$embulkVersion" + testCompile "org.embulk:embulk-junit4:$embulkVersion" testCompile "org.embulk:embulk-core:$embulkVersion:tests" + testCompile "org.embulk:embulk-deps:$embulkVersion" } checkstyle { diff --git a/gradle/dependency-locks/embulkPluginRuntime.lockfile b/gradle/dependency-locks/embulkPluginRuntime.lockfile index c252ca7..d60f9d7 100644 --- a/gradle/dependency-locks/embulkPluginRuntime.lockfile +++ b/gradle/dependency-locks/embulkPluginRuntime.lockfile @@ -1,13 +1,24 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. +com.fasterxml.jackson.core:jackson-annotations:2.6.7 +com.fasterxml.jackson.core:jackson-core:2.6.7 +com.fasterxml.jackson.core:jackson-databind:2.6.7 +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.6.7 com.google.code.gson:gson:2.8.5 +com.google.guava:guava:18.0 commons-codec:commons-codec:1.10 commons-logging:commons-logging:1.2 javax.annotation:javax.annotation-api:1.2 +javax.validation:validation-api:1.1.0.Final javax.ws.rs:javax.ws.rs-api:2.1 org.apache.httpcomponents:httpclient:4.5.6 org.apache.httpcomponents:httpcore:4.4.10 +org.embulk:embulk-util-config:0.1.4 +org.embulk:embulk-util-json:0.1.0 +org.embulk:embulk-util-retryhelper:0.8.1 +org.embulk:embulk-util-rubytime:0.3.2 +org.embulk:embulk-util-timestamp:0.2.1 org.glassfish.hk2.external:javax.inject:2.5.0-b42 org.glassfish.hk2:osgi-resource-locator:1.0.1 org.glassfish.jersey.core:jersey-client:2.27 diff --git a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java index 148b2c9..7ab8e01 100644 --- a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java +++ b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java @@ -8,25 +8,30 @@ import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; - -import org.embulk.config.Config; -import org.embulk.config.ConfigDefault; import org.embulk.config.ConfigDiff; import org.embulk.config.ConfigException; import org.embulk.config.ConfigSource; -import org.embulk.config.Task; import org.embulk.config.TaskReport; import org.embulk.config.TaskSource; import org.embulk.exec.GuessExecutor; import org.embulk.input.jira.client.JiraClient; import org.embulk.input.jira.util.JiraUtil; import org.embulk.spi.Buffer; +import org.embulk.spi.BufferAllocator; import org.embulk.spi.Exec; +import org.embulk.spi.ExecInternal; import org.embulk.spi.InputPlugin; import org.embulk.spi.PageBuilder; import org.embulk.spi.PageOutput; import org.embulk.spi.Schema; import org.embulk.spi.SchemaConfig; +import org.embulk.util.config.Config; +import org.embulk.util.config.ConfigDefault; +import org.embulk.util.config.ConfigMapper; +import org.embulk.util.config.ConfigMapperFactory; +import org.embulk.util.config.Task; +import org.embulk.util.config.TaskMapper; +import org.embulk.util.config.modules.TypeModule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +51,16 @@ public class JiraInputPlugin implements InputPlugin { private static final Logger LOGGER = LoggerFactory.getLogger(JiraInputPlugin.class); + @VisibleForTesting + public static final ConfigMapperFactory CONFIG_MAPPER_FACTORY = ConfigMapperFactory + .builder() + .addDefaultModules() + // required for PluginTask > SchemaConfig > ColumnConfig > Type + .addModule(new TypeModule()) + .build(); + @VisibleForTesting + public static final ConfigMapper CONFIG_MAPPER = CONFIG_MAPPER_FACTORY.createConfigMapper(); + private static final TaskMapper TASK_MAPPER = CONFIG_MAPPER_FACTORY.createTaskMapper(); public interface PluginTask extends Task @@ -92,12 +107,12 @@ public interface PluginTask public ConfigDiff transaction(final ConfigSource config, final InputPlugin.Control control) { - final PluginTask task = config.loadConfig(PluginTask.class); + final PluginTask task = CONFIG_MAPPER.map(config, PluginTask.class); final Schema schema = task.getColumns().toSchema(); final int taskCount = 1; - return resume(task.dump(), schema, taskCount, control); + return resume(task.toTaskSource(), schema, taskCount, control); } @Override @@ -106,7 +121,7 @@ public ConfigDiff resume(final TaskSource taskSource, final InputPlugin.Control control) { control.run(taskSource, schema, taskCount); - return Exec.newConfigDiff(); + return CONFIG_MAPPER_FACTORY.newConfigDiff(); } @Override @@ -121,7 +136,7 @@ public TaskReport run(final TaskSource taskSource, final Schema schema, final int taskIndex, final PageOutput output) { - final PluginTask task = taskSource.loadTask(PluginTask.class); + final PluginTask task = TASK_MAPPER.map(taskSource, PluginTask.class); JiraUtil.validateTaskConfig(task); final JiraClient jiraClient = getJiraClient(); jiraClient.checkUserCredentials(task); @@ -144,7 +159,7 @@ public TaskReport run(final TaskSource taskSource, } pageBuilder.finish(); } - return Exec.newTaskReport(); + return CONFIG_MAPPER_FACTORY.newTaskReport(); } @Override @@ -152,7 +167,7 @@ public ConfigDiff guess(final ConfigSource config) { // Reset columns in case already have or missing on configuration config.set("columns", new ObjectMapper().createArrayNode()); - final PluginTask task = config.loadConfig(PluginTask.class); + final PluginTask task = CONFIG_MAPPER.map(config, PluginTask.class); JiraUtil.validateTaskConfig(task); final JiraClient jiraClient = getJiraClient(); jiraClient.checkUserCredentials(task); @@ -160,18 +175,22 @@ public ConfigDiff guess(final ConfigSource config) if (issues.isEmpty()) { throw new ConfigException("Could not guess schema due to empty data set"); } + + // TODO: Eliminate the use of ExecInternal.getInjector that soon will be obsolete. final Buffer sample = Buffer.copyOf(createSamples(issues, getUniqueAttributes(issues)).toString().getBytes()); - final JsonNode columns = Exec.getInjector().getInstance(GuessExecutor.class) - .guessParserConfig(sample, Exec.newConfigSource(), createGuessConfig()) - .get(JsonNode.class, "columns"); - return Exec.newConfigDiff().set("columns", columns); + final JsonNode columns = ExecInternal.getInjector().getInstance(GuessExecutor.class) + .guessParserConfig(sample, Exec.newConfigSource(), createGuessConfig()) + .get(JsonNode.class, "columns"); + + return CONFIG_MAPPER_FACTORY.newConfigDiff().set("columns", columns); } private ConfigSource createGuessConfig() { + // TODO: there will be no need for this after "Eliminate the use of ExecInternal.getInjector that soon will be obsolete." return Exec.newConfigSource() - .set("guess_plugins", ImmutableList.of("jira")) - .set("guess_sample_buffer_bytes", GUESS_BUFFER_SIZE); + .set("guess_plugins", ImmutableList.of("jira")) + .set("guess_sample_buffer_bytes", GUESS_BUFFER_SIZE); } private SortedSet getUniqueAttributes(final List issues) @@ -206,13 +225,20 @@ private JsonArray createSamples(final List issues, final Set uniq @VisibleForTesting public GuessExecutor getGuessExecutor() { - return Exec.getInjector().getInstance(GuessExecutor.class); + return ExecInternal.getInjector().getInstance(GuessExecutor.class); } + @SuppressWarnings("deprecation") @VisibleForTesting public PageBuilder getPageBuilder(final Schema schema, final PageOutput output) { - return new PageBuilder(Exec.getBufferAllocator(), schema, output); + if (HAS_EXEC_GET_PAGE_BUILDER) { + return Exec.getPageBuilder(Exec.getBufferAllocator(), schema, output); + } + else { + // To be compatible to run on Embulk belows v0.10.17 + return new PageBuilder(Exec.getBufferAllocator(), schema, output); + } } @VisibleForTesting @@ -226,4 +252,16 @@ public JiraClient getJiraClient() { return new JiraClient(); } + + private static boolean hasExecGetPageBuilder() + { + try { + Exec.class.getMethod("getPageBuilder", BufferAllocator.class, Schema.class, PageOutput.class); + } + catch (final NoSuchMethodException ex) { + return false; + } + return true; + } + private static final boolean HAS_EXEC_GET_PAGE_BUILDER = hasExecGetPageBuilder(); } diff --git a/src/main/java/org/embulk/input/jira/client/JiraClient.java b/src/main/java/org/embulk/input/jira/client/JiraClient.java index b4f139f..ffd3cd9 100644 --- a/src/main/java/org/embulk/input/jira/client/JiraClient.java +++ b/src/main/java/org/embulk/input/jira/client/JiraClient.java @@ -6,7 +6,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; - import org.apache.http.HttpStatus; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; @@ -23,8 +22,9 @@ import org.embulk.input.jira.JiraInputPlugin.PluginTask; import org.embulk.input.jira.util.JiraException; import org.embulk.input.jira.util.JiraUtil; -import org.embulk.spi.util.RetryExecutor.RetryGiveupException; -import org.embulk.spi.util.RetryExecutor.Retryable; +import org.embulk.util.retryhelper.RetryExecutor; +import org.embulk.util.retryhelper.RetryGiveupException; +import org.embulk.util.retryhelper.Retryable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,7 +43,6 @@ import static org.apache.http.HttpHeaders.CONTENT_TYPE; import static org.embulk.input.jira.Constant.HTTP_TIMEOUT; import static org.embulk.input.jira.Constant.MIN_RESULTS; -import static org.embulk.spi.util.RetryExecutor.retryExecutor; public class JiraClient { @@ -94,11 +93,13 @@ public int getTotalCount(final PluginTask task) private String searchJiraAPI(final PluginTask task, final int startAt, final int maxResults) { try { - return retryExecutor().withRetryLimit(task.getRetryLimit()) - .withInitialRetryWait(task.getInitialRetryIntervalMillis()) - .withMaxRetryWait(task.getMaximumRetryIntervalMillis()) - .runInterruptible(new Retryable() - { + return RetryExecutor.builder() + .withRetryLimit(task.getRetryLimit()) + .withInitialRetryWaitMillis(task.getInitialRetryIntervalMillis()) + .withMaxRetryWaitMillis(task.getMaximumRetryIntervalMillis()) + .build() + .runInterruptible(new Retryable() + { @Override public String call() throws Exception { diff --git a/src/main/java/org/embulk/input/jira/util/JiraUtil.java b/src/main/java/org/embulk/input/jira/util/JiraUtil.java index d560599..7fa4a1d 100644 --- a/src/main/java/org/embulk/input/jira/util/JiraUtil.java +++ b/src/main/java/org/embulk/input/jira/util/JiraUtil.java @@ -1,7 +1,6 @@ package org.embulk.input.jira.util; import com.google.gson.JsonElement; - import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; @@ -17,13 +16,13 @@ import org.embulk.spi.ColumnVisitor; import org.embulk.spi.PageBuilder; import org.embulk.spi.Schema; -import org.embulk.spi.json.JsonParser; -import org.embulk.spi.time.Timestamp; -import org.embulk.spi.time.TimestampParser; +import org.embulk.util.json.JsonParser; +import org.embulk.util.timestamp.TimestampFormatter; import javax.ws.rs.core.UriBuilder; import java.io.IOException; +import java.time.Instant; import java.util.List; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -97,7 +96,7 @@ public static void validateTaskConfig(final PluginTask task) * For getting the timestamp value of the node * Sometime if the parser could not parse the value then return null * */ - private static Timestamp getTimestampValue(final PluginTask task, final Column column, final String value) + private static Instant getTimestampValue(final PluginTask task, final Column column, final String value) { final List columnConfigs = task.getColumns().getColumns(); String pattern = DEFAULT_TIMESTAMP_PATTERN; @@ -110,10 +109,13 @@ private static Timestamp getTimestampValue(final PluginTask task, final Column c break; } } - final TimestampParser parser = TimestampParser.of(pattern, "UTC"); - Timestamp result = null; + final TimestampFormatter formatter = TimestampFormatter + .builder(pattern, true) + .setDefaultZoneFromString("UTC") + .build(); + Instant result = null; try { - result = parser.parse(value); + result = formatter.parse(value); } catch (final Exception e) { } @@ -215,7 +217,7 @@ public void timestampColumn(final Column column) pageBuilder.setNull(column); } else { - final Timestamp value = getTimestampValue(task, column, data.getAsString()); + final Instant value = getTimestampValue(task, column, data.getAsString()); if (value == null) { pageBuilder.setNull(column); } diff --git a/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java b/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java index be54845..3380906 100644 --- a/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java +++ b/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.List; +import static org.embulk.input.jira.JiraInputPlugin.CONFIG_MAPPER; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.times; @@ -57,9 +58,8 @@ public void setUp() throws IOException jiraClient = Mockito.spy(new JiraClient()); data = TestHelpers.getJsonFromFile("jira_input_plugin.json"); config = TestHelpers.config(); - config.loadConfig(PluginTask.class); + CONFIG_MAPPER.map(config, PluginTask.class); pageBuilder = Mockito.mock(PageBuilder.class); - //pageBuilder = new PageBuilder(Exec.getBufferAllocator(), config.loadConfig(PluginTask.class).getColumns().toSchema(), output); } when(plugin.getJiraClient()).thenReturn(jiraClient); when(jiraClient.createHttpClient()).thenReturn(client); @@ -75,14 +75,14 @@ public void test_run_withEmptyResult() throws IOException JsonObject searchResponse = data.get("emptyResult").getAsJsonObject(); when(statusLine.getStatusCode()) - .thenReturn(authorizeResponse.get("statusCode").getAsInt()) - .thenReturn(searchResponse.get("statusCode").getAsInt()); + .thenReturn(authorizeResponse.get("statusCode").getAsInt()) + .thenReturn(searchResponse.get("statusCode").getAsInt()); when(response.getEntity()) - .thenReturn(new StringEntity(authorizeResponse.get("body").toString())) - .thenReturn(new StringEntity(searchResponse.get("body").toString())); + .thenReturn(new StringEntity(authorizeResponse.get("body").toString())) + .thenReturn(new StringEntity(searchResponse.get("body").toString())); plugin.transaction(config, new Control()); - // Check credential 1 + getTotal 1 + loadData 0 + // Check credential 1 + getTotal 1 + loadData 0 verify(jiraClient, times(2)).createHttpClient(); verify(pageBuilder, times(0)).addRecord(); verify(pageBuilder, times(1)).finish(); @@ -95,11 +95,11 @@ public void test_run_with1RecordsResult() throws IOException JsonObject searchResponse = data.get("oneRecordResult").getAsJsonObject(); when(statusLine.getStatusCode()) - .thenReturn(authorizeResponse.get("statusCode").getAsInt()) - .thenReturn(searchResponse.get("statusCode").getAsInt()); + .thenReturn(authorizeResponse.get("statusCode").getAsInt()) + .thenReturn(searchResponse.get("statusCode").getAsInt()); when(response.getEntity()) - .thenReturn(new StringEntity(authorizeResponse.get("body").toString())) - .thenReturn(new StringEntity(searchResponse.get("body").toString())); + .thenReturn(new StringEntity(authorizeResponse.get("body").toString())) + .thenReturn(new StringEntity(searchResponse.get("body").toString())); plugin.transaction(config, new Control()); // Check credential 1 + getTotal 1 + loadData 1 @@ -115,11 +115,11 @@ public void test_run_with2PagesResult() throws IOException JsonObject searchResponse = data.get("2PagesResult").getAsJsonObject(); when(statusLine.getStatusCode()) - .thenReturn(authorizeResponse.get("statusCode").getAsInt()) - .thenReturn(searchResponse.get("statusCode").getAsInt()); + .thenReturn(authorizeResponse.get("statusCode").getAsInt()) + .thenReturn(searchResponse.get("statusCode").getAsInt()); when(response.getEntity()) - .thenReturn(new StringEntity(authorizeResponse.get("body").toString())) - .thenReturn(new StringEntity(searchResponse.get("body").toString())); + .thenReturn(new StringEntity(authorizeResponse.get("body").toString())) + .thenReturn(new StringEntity(searchResponse.get("body").toString())); plugin.transaction(config, new Control()); // Check credential 1 + getTotal 1 + loadData 2 @@ -136,10 +136,10 @@ public void test_preview_withEmptyResult() throws IOException JsonObject searchResponse = data.get("emptyResult").getAsJsonObject(); when(statusLine.getStatusCode()) - .thenReturn(authorizeResponse.get("statusCode").getAsInt()) - .thenReturn(searchResponse.get("statusCode").getAsInt()); + .thenReturn(authorizeResponse.get("statusCode").getAsInt()) + .thenReturn(searchResponse.get("statusCode").getAsInt()); when(response.getEntity()) - .thenReturn(new StringEntity(searchResponse.get("body").toString())); + .thenReturn(new StringEntity(searchResponse.get("body").toString())); plugin.transaction(config, new Control()); // Check credential 1 + loadData 1 @@ -156,11 +156,11 @@ public void test_preview_with1RecordsResult() throws IOException JsonObject searchResponse = data.get("oneRecordResult").getAsJsonObject(); when(statusLine.getStatusCode()) - .thenReturn(authorizeResponse.get("statusCode").getAsInt()) - .thenReturn(searchResponse.get("statusCode").getAsInt()); + .thenReturn(authorizeResponse.get("statusCode").getAsInt()) + .thenReturn(searchResponse.get("statusCode").getAsInt()); when(response.getEntity()) - .thenReturn(new StringEntity(authorizeResponse.get("body").toString())) - .thenReturn(new StringEntity(searchResponse.get("body").toString())); + .thenReturn(new StringEntity(authorizeResponse.get("body").toString())) + .thenReturn(new StringEntity(searchResponse.get("body").toString())); plugin.transaction(config, new Control()); // Check credential 1 + loadData 1 @@ -172,16 +172,22 @@ public void test_preview_with1RecordsResult() throws IOException @Test public void test_guess() throws IOException { + // ConfigSource configSource = TestHelpers.config( + // new ConfigLoader( + // // TODO: What about TypeJacksonModule? + // new ModelManager(null, new ObjectMapper().registerModule(new Jdk8Module())) + // ).newConfigSource()); ConfigSource configSource = TestHelpers.config(); + JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); JsonObject searchResponse = data.get("guessDataResult").getAsJsonObject(); when(statusLine.getStatusCode()) - .thenReturn(authorizeResponse.get("statusCode").getAsInt()) - .thenReturn(searchResponse.get("statusCode").getAsInt()); + .thenReturn(authorizeResponse.get("statusCode").getAsInt()) + .thenReturn(searchResponse.get("statusCode").getAsInt()); when(response.getEntity()) - .thenReturn(new StringEntity(searchResponse.get("body").toString())) - .thenReturn(new StringEntity(searchResponse.get("body").toString())); + .thenReturn(new StringEntity(searchResponse.get("body").toString())) + .thenReturn(new StringEntity(searchResponse.get("body").toString())); ConfigDiff result = plugin.guess(configSource); JsonElement expected = data.get("guessResult").getAsJsonObject(); diff --git a/src/test/java/org/embulk/input/jira/JiraPluginTestRuntime.java b/src/test/java/org/embulk/input/jira/JiraPluginTestRuntime.java index 38b8a8c..5114c14 100644 --- a/src/test/java/org/embulk/input/jira/JiraPluginTestRuntime.java +++ b/src/test/java/org/embulk/input/jira/JiraPluginTestRuntime.java @@ -1,13 +1,10 @@ package org.embulk.input.jira; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.inject.Binder; import com.google.inject.Injector; import com.google.inject.Module; +import org.embulk.EmbulkSystemProperties; import org.embulk.GuiceBinder; -import org.embulk.RandomManager; import org.embulk.TestPluginSourceModule; import org.embulk.TestUtilityModule; import org.embulk.config.ConfigSource; @@ -17,102 +14,71 @@ import org.embulk.exec.ExtensionServiceLoaderModule; import org.embulk.exec.SystemConfigModule; import org.embulk.jruby.JRubyScriptingModule; -import org.embulk.plugin.BuiltinPluginSourceModule; -import org.embulk.plugin.PluginClassLoaderFactory; -import org.embulk.spi.BufferAllocator; -import org.embulk.spi.Exec; import org.embulk.spi.ExecAction; -import org.embulk.spi.ExecSession; +import org.embulk.spi.ExecInternal; +import org.embulk.spi.ExecSessionInternal; import org.junit.runner.Description; import org.junit.runners.model.Statement; -import java.util.Random; +import java.util.Properties; /** * This is a clone from {@link org.embulk.EmbulkTestRuntime}, since there is no easy way to extend it. - * The only modification is on the provided systemConfig, enable tests to run `embulk/guess/jira.rb` + * The sole modification is the providing of "jruby_load_path" system property. + * This will enable tests to able to locate for guess plugins. */ public class JiraPluginTestRuntime extends GuiceBinder { - private static ConfigSource getSystemConfig() - { - final ObjectNode configNode = JsonNodeFactory.instance.objectNode(); - configNode.set("jruby_load_path", JsonNodeFactory.instance.arrayNode().add("lib")); - - return new DataSourceImpl(new ModelManager(null, new ObjectMapper()), configNode); - } - public static class TestRuntimeModule implements Module { @Override - public void configure(final Binder binder) + public void configure(Binder binder) { - final ConfigSource systemConfig = getSystemConfig(); - new SystemConfigModule(systemConfig).configure(binder); - new ExecModule(systemConfig).configure(binder); - new ExtensionServiceLoaderModule(systemConfig).configure(binder); - new BuiltinPluginSourceModule().configure(binder); - new JRubyScriptingModule(systemConfig).configure(binder); + final EmbulkSystemProperties embulkSystemProperties = EmbulkSystemProperties.of(new Properties() + { + { + setProperty("jruby_load_path", "lib"); + } + }); + new SystemConfigModule(embulkSystemProperties).configure(binder); + new ExecModule(embulkSystemProperties).configure(binder); + new ExtensionServiceLoaderModule(embulkSystemProperties).configure(binder); + new JRubyScriptingModule().configure(binder); new TestUtilityModule().configure(binder); new TestPluginSourceModule().configure(binder); } } - private final ExecSession exec; + private ExecSessionInternal exec; public JiraPluginTestRuntime() { - super(new TestRuntimeModule()); - final Injector injector = getInjector(); - final ConfigSource execConfig = new DataSourceImpl(injector.getInstance(ModelManager.class)); - this.exec = ExecSession.builder(injector).fromExecConfig(execConfig).build(); - } - - public ExecSession getExec() - { - return exec; - } - - public BufferAllocator getBufferAllocator() - { - return getInstance(BufferAllocator.class); - } - - public ModelManager getModelManager() - { - return getInstance(ModelManager.class); - } - - public Random getRandom() - { - return getInstance(RandomManager.class).getRandom(); - } - - public PluginClassLoaderFactory getPluginClassLoaderFactory() - { - return getInstance(PluginClassLoaderFactory.class); + super(new JiraPluginTestRuntime.TestRuntimeModule()); + Injector injector = getInjector(); + ConfigSource execConfig = new DataSourceImpl(injector.getInstance(ModelManager.class)); + this.exec = ExecSessionInternal.builderInternal(injector).fromExecConfig(execConfig).build(); } @Override - public Statement apply(final Statement base, final Description description) + public Statement apply(Statement base, Description description) { final Statement superStatement = JiraPluginTestRuntime.super.apply(base, description); - return new Statement() { - @Override + return new Statement() + { public void evaluate() throws Throwable { try { - Exec.doWith(exec, (ExecAction) () -> { + ExecInternal.doWith(exec, (ExecAction) () -> { try { superStatement.evaluate(); } - catch (final Throwable ex) { + catch (Throwable ex) { throw new RuntimeExecutionException(ex); } return null; }); } - catch (final RuntimeException ex) { + catch (RuntimeException ex) { throw ex.getCause(); } finally { @@ -124,7 +90,7 @@ public void evaluate() throws Throwable private static class RuntimeExecutionException extends RuntimeException { - public RuntimeExecutionException(final Throwable cause) + public RuntimeExecutionException(Throwable cause) { super(cause); } diff --git a/src/test/java/org/embulk/input/jira/TestHelpers.java b/src/test/java/org/embulk/input/jira/TestHelpers.java index f6c0c69..7bb0b03 100644 --- a/src/test/java/org/embulk/input/jira/TestHelpers.java +++ b/src/test/java/org/embulk/input/jira/TestHelpers.java @@ -1,27 +1,28 @@ package org.embulk.input.jira; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.guava.GuavaModule; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.google.common.io.Resources; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.stream.JsonReader; - -import org.embulk.config.ConfigLoader; import org.embulk.config.ConfigSource; -import org.embulk.config.ModelManager; +import org.embulk.spi.ColumnConfig; +import org.embulk.spi.SchemaConfig; +import org.embulk.spi.type.Types; -import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.util.ArrayList; + +import static org.embulk.input.jira.JiraInputPlugin.CONFIG_MAPPER_FACTORY; public final class TestHelpers { - private TestHelpers() {} + private TestHelpers() + { + } public static JsonObject getJsonFromFile(final String fileName) throws IOException - { + { final String path = Resources.getResource(fileName).getPath(); try (JsonReader reader = new JsonReader(new FileReader(path))) { final JsonParser parser = new JsonParser(); @@ -29,13 +30,29 @@ public static JsonObject getJsonFromFile(final String fileName) throws IOExcepti } } - public static ConfigSource config() throws IOException + public static ConfigSource config() { - final String path = Resources.getResource("config.yml").getPath(); - final ObjectMapper mapper = new ObjectMapper() - .registerModule(new GuavaModule()) - .registerModule(new Jdk8Module()); - final ConfigLoader configLoader = new ConfigLoader(new ModelManager(null, mapper)); - return configLoader.fromYamlFile(new File(path)); + return CONFIG_MAPPER_FACTORY.newConfigSource() + .set("type", "jira") + .set("username", "example@example.com") + .set("password", "XXXXXXXXXXXXXXXXX") + .set("uri", "https://example.com/") + .set("jql", "project = example") + .set("retry_limit", 3) + .set("columns", new SchemaConfig(new ArrayList() + { + { + add(new ColumnConfig("boolean", Types.BOOLEAN, EMPTY_CONFIG_SOURCE)); + add(new ColumnConfig("long", Types.LONG, EMPTY_CONFIG_SOURCE)); + add(new ColumnConfig("double", Types.DOUBLE, EMPTY_CONFIG_SOURCE)); + add(new ColumnConfig("string", Types.STRING, EMPTY_CONFIG_SOURCE)); + add(new ColumnConfig("date", + Types.TIMESTAMP, CONFIG_MAPPER_FACTORY + .newConfigSource().set("format", "%Y-%m-%dT%H:%M:%S.%L%z"))); + add(new ColumnConfig("json", Types.JSON, EMPTY_CONFIG_SOURCE)); + } + })); } + + private static final ConfigSource EMPTY_CONFIG_SOURCE = CONFIG_MAPPER_FACTORY.newConfigSource(); } diff --git a/src/test/java/org/embulk/input/jira/client/JiraClientTest.java b/src/test/java/org/embulk/input/jira/client/JiraClientTest.java index 54f9ecf..9562ade 100644 --- a/src/test/java/org/embulk/input/jira/client/JiraClientTest.java +++ b/src/test/java/org/embulk/input/jira/client/JiraClientTest.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.List; +import static org.embulk.input.jira.JiraInputPlugin.CONFIG_MAPPER; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.times; @@ -45,7 +46,7 @@ public void setUp() throws IOException if (jiraClient == null) { jiraClient = Mockito.spy(new JiraClient()); response = Mockito.mock(CloseableHttpResponse.class); - task = TestHelpers.config().loadConfig(PluginTask.class); + task = CONFIG_MAPPER.map(TestHelpers.config(), PluginTask.class); data = TestHelpers.getJsonFromFile("jira_client.json"); } when(jiraClient.createHttpClient()).thenReturn(client); @@ -233,13 +234,13 @@ public void test_searchIssues_emptyJql() throws IOException when(statusLine.getStatusCode()).thenReturn(statusCode); when(response.getEntity()).thenReturn(new StringEntity(body)); ConfigSource config = TestHelpers.config().remove("jql"); - task = config.loadConfig(PluginTask.class); + task = CONFIG_MAPPER.map(config, PluginTask.class); List issues = jiraClient.searchIssues(task, 0, 50); assertEquals(issues.size(), 2); config = TestHelpers.config().set("jql", ""); - task = config.loadConfig(PluginTask.class); + task = CONFIG_MAPPER.map(config, PluginTask.class); issues = jiraClient.searchIssues(task, 0, 50); assertEquals(issues.size(), 2); diff --git a/src/test/java/org/embulk/input/jira/util/JiraUtilTest.java b/src/test/java/org/embulk/input/jira/util/JiraUtilTest.java index ca3012f..460aa80 100644 --- a/src/test/java/org/embulk/input/jira/util/JiraUtilTest.java +++ b/src/test/java/org/embulk/input/jira/util/JiraUtilTest.java @@ -1,7 +1,6 @@ package org.embulk.input.jira.util; import com.google.gson.JsonObject; - import org.embulk.config.ConfigException; import org.embulk.config.ConfigSource; import org.embulk.input.jira.Issue; @@ -10,16 +9,17 @@ import org.embulk.spi.Column; import org.embulk.spi.PageBuilder; import org.embulk.spi.Schema; -import org.embulk.spi.json.JsonParser; -import org.embulk.spi.time.Timestamp; -import org.embulk.spi.time.TimestampParser; +import org.embulk.util.json.JsonParser; +import org.embulk.util.timestamp.TimestampFormatter; import org.junit.BeforeClass; import org.junit.Test; import org.mockito.Mockito; import org.msgpack.value.Value; import java.io.IOException; +import java.time.Instant; +import static org.embulk.input.jira.JiraInputPlugin.CONFIG_MAPPER; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.times; @@ -41,7 +41,7 @@ public class JiraUtilTest public static void setUp() throws IOException { data = TestHelpers.getJsonFromFile("jira_util.json"); - pluginTask = TestHelpers.config().loadConfig(PluginTask.class); + pluginTask = CONFIG_MAPPER.map(TestHelpers.config(), PluginTask.class); schema = pluginTask.getColumns().toSchema(); booleanColumn = schema.getColumn(0); longColumn = schema.getColumn(1); @@ -50,6 +50,7 @@ public static void setUp() throws IOException dateColumn = schema.getColumn(4); jsonColumn = schema.getColumn(5); } + @Test public void test_calculateTotalPage() { @@ -102,7 +103,7 @@ public void test_buildPermissionUrl() @Test public void test_buildSearchUrl() throws IOException { - PluginTask task = TestHelpers.config().loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(TestHelpers.config(), PluginTask.class); String expected = "https://example.com/rest/api/latest/search"; String actual = JiraUtil.buildSearchUrl(task.getUri()); assertEquals(expected, actual); @@ -112,7 +113,7 @@ public void test_buildSearchUrl() throws IOException public void test_validateTaskConfig_allValid() throws IOException { ConfigSource configSource = TestHelpers.config(); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); } @@ -122,7 +123,7 @@ public void test_validateTaskConfig_emptyUsername() throws IOException ConfigException exception = assertThrows("Username or email could not be empty", ConfigException.class, () -> { ConfigSource configSource = TestHelpers.config(); configSource.set("username", ""); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); }); assertEquals("Username or email could not be empty", exception.getMessage()); @@ -134,7 +135,7 @@ public void test_validateTaskConfig_emptyPassword() throws IOException ConfigException exception = assertThrows(ConfigException.class, () -> { ConfigSource configSource = TestHelpers.config(); configSource.set("password", ""); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); }); assertEquals("Password could not be empty", exception.getMessage()); @@ -146,7 +147,7 @@ public void test_validateTaskConfig_emptyUri() throws IOException ConfigException exception = assertThrows(ConfigException.class, () -> { ConfigSource configSource = TestHelpers.config(); configSource.set("uri", ""); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); }); assertEquals("JIRA API endpoint could not be empty", exception.getMessage()); @@ -158,7 +159,7 @@ public void test_validateTaskConfig_nonExistedUri() throws IOException ConfigException exception = assertThrows(ConfigException.class, () -> { ConfigSource configSource = TestHelpers.config(); configSource.set("uri", "https://not-existed-domain"); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); }); assertEquals("JIRA API endpoint is incorrect or not available", exception.getMessage()); @@ -170,7 +171,7 @@ public void test_validateTaskConfig_invalidUriProtocol() throws IOException ConfigException exception = assertThrows(ConfigException.class, () -> { ConfigSource configSource = TestHelpers.config(); configSource.set("uri", "ftp://example.com"); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); }); assertEquals("JIRA API endpoint is incorrect or not available", exception.getMessage()); @@ -182,7 +183,7 @@ public void test_validateTaskConfig_containSpaceUri() throws IOException ConfigException exception = assertThrows(ConfigException.class, () -> { ConfigSource configSource = TestHelpers.config(); configSource.set("uri", "https://example .com"); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); }); assertEquals("JIRA API endpoint is incorrect or not available", exception.getMessage()); @@ -193,7 +194,7 @@ public void test_validateTaskConfig_emptyJql() throws IOException { ConfigSource configSource = TestHelpers.config(); configSource.set("jql", ""); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); } @@ -202,7 +203,7 @@ public void test_validateTaskConfig_missingJql() throws IOException { ConfigSource configSource = TestHelpers.config(); configSource.remove("jql"); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); } @@ -212,7 +213,7 @@ public void test_validateTaskConfig_RetryIntervalIs0() throws IOException ConfigException exception = assertThrows(ConfigException.class, () -> { ConfigSource configSource = TestHelpers.config(); configSource.set("initial_retry_interval_millis", 0); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); }); assertEquals("Initial retry delay should be equal or greater than 1", exception.getMessage()); @@ -224,7 +225,7 @@ public void test_validateTaskConfig_RetryIntervalIsNegative() throws IOException ConfigException exception = assertThrows(ConfigException.class, () -> { ConfigSource configSource = TestHelpers.config(); configSource.set("initial_retry_interval_millis", -1); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); }); assertEquals("Initial retry delay should be equal or greater than 1", exception.getMessage()); @@ -236,7 +237,7 @@ public void test_validateTaskConfig_RetryLimitGreaterThan10() throws IOException ConfigException exception = assertThrows(ConfigException.class, () -> { ConfigSource configSource = TestHelpers.config(); configSource.set("retry_limit", 11); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); }); assertEquals("Retry limit should between 0 and 10", exception.getMessage()); @@ -248,7 +249,7 @@ public void test_validateTaskConfig_RetryLimitLessThan0() throws IOException ConfigException exception = assertThrows(ConfigException.class, () -> { ConfigSource configSource = TestHelpers.config(); configSource.set("retry_limit", -1); - PluginTask task = configSource.loadConfig(PluginTask.class); + PluginTask task = CONFIG_MAPPER.map(configSource, PluginTask.class); JiraUtil.validateTaskConfig(task); }); assertEquals("Retry limit should between 0 and 10", exception.getMessage()); @@ -265,7 +266,10 @@ public void test_addRecord_allRight() Long longValue = Long.valueOf(1); Double doubleValue = Double.valueOf(1); String stringValue = "string"; - Timestamp dateValue = TimestampParser.of("%Y-%m-%dT%H:%M:%S.%L%z", "UTC").parse("2019-01-01T00:00:00.000Z"); + Instant dateValue = TimestampFormatter + .builder("%Y-%m-%dT%H:%M:%S.%L%z", true) + .setDefaultZoneFromString("UTC") + .build().parse("2019-01-01T00:00:00.000Z"); Value jsonValue = new JsonParser().parse("{}"); JiraUtil.addRecord(issue, schema, pluginTask, mock); diff --git a/src/test/resources/config.yml b/src/test/resources/config.yml deleted file mode 100644 index a37d6a2..0000000 --- a/src/test/resources/config.yml +++ /dev/null @@ -1,13 +0,0 @@ -type: jira -username: example@example.com -password: XXXXXXXXXXXXXXXXX -uri: "https://example.com/" -jql: project = example -retry_limit: 3 -columns: - - {name: boolean, type: boolean} - - {name: long, type: long} - - {name: double, type: double} - - {name: string, type: string} - - {name: date, type: timestamp, format: '%Y-%m-%dT%H:%M:%S.%L%z'} - - {name: json, type: json} \ No newline at end of file From 3ae3eb5d5274576abaa50cb8e9923a005283d236 Mon Sep 17 00:00:00 2001 From: huylenq Date: Thu, 12 Nov 2020 13:29:34 +0700 Subject: [PATCH 06/38] Lock commons-lang at 3.4 This is necessary to be compatible with Embulk v0.10.* --- build.gradle | 1 + gradle/dependency-locks/embulkPluginRuntime.lockfile | 1 + 2 files changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 5b9fca6..03fce59 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,7 @@ dependencies { compile group: 'org.glassfish.jersey.core', name: 'jersey-client', version: '2.27' compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5' compile 'com.google.guava:guava:18.0' + compile 'org.apache.commons:commons-lang3:3.4' testCompile "junit:junit:4.+" testCompile "org.mockito:mockito-core:2.+" diff --git a/gradle/dependency-locks/embulkPluginRuntime.lockfile b/gradle/dependency-locks/embulkPluginRuntime.lockfile index d60f9d7..0709cac 100644 --- a/gradle/dependency-locks/embulkPluginRuntime.lockfile +++ b/gradle/dependency-locks/embulkPluginRuntime.lockfile @@ -12,6 +12,7 @@ commons-logging:commons-logging:1.2 javax.annotation:javax.annotation-api:1.2 javax.validation:validation-api:1.1.0.Final javax.ws.rs:javax.ws.rs-api:2.1 +org.apache.commons:commons-lang3:3.4 org.apache.httpcomponents:httpclient:4.5.6 org.apache.httpcomponents:httpcore:4.4.10 org.embulk:embulk-util-config:0.1.4 From dfe461d913be929fc2f589166c6932e9b3c4303c Mon Sep 17 00:00:00 2001 From: huylenq Date: Fri, 13 Nov 2020 12:35:12 +0700 Subject: [PATCH 07/38] Includes some JavaEE dependencies for jersey-client --- build.gradle | 11 +++++++++-- gradle/dependency-locks/embulkPluginRuntime.lockfile | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 03fce59..89e6600 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ repositories { jcenter() } -version = "0.2.11.alpha.0" +version = "0.2.11.SNAPSHOT" group = "com.treasuredata.embulk.plugins" description = "JIRA Embulk input plugin." @@ -53,7 +53,14 @@ dependencies { compile 'javax.validation:validation-api:1.1.0.Final' compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.6' + compile group: 'org.glassfish.jersey.core', name: 'jersey-client', version: '2.27' + // Java EE dependencies (required on Java 9+) for jersey-client + compile "javax.xml.bind:jaxb-api:2.2.11" + compile "com.sun.xml.bind:jaxb-core:2.2.11" + compile "com.sun.xml.bind:jaxb-impl:2.2.11" + compile "javax.activation:activation:1.1.1" + compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5' compile 'com.google.guava:guava:18.0' compile 'org.apache.commons:commons-lang3:3.4' @@ -115,4 +122,4 @@ gem { gemPush { host = "https://rubygems.org" -} \ No newline at end of file +} diff --git a/gradle/dependency-locks/embulkPluginRuntime.lockfile b/gradle/dependency-locks/embulkPluginRuntime.lockfile index 0709cac..c1debb5 100644 --- a/gradle/dependency-locks/embulkPluginRuntime.lockfile +++ b/gradle/dependency-locks/embulkPluginRuntime.lockfile @@ -7,11 +7,15 @@ com.fasterxml.jackson.core:jackson-databind:2.6.7 com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.6.7 com.google.code.gson:gson:2.8.5 com.google.guava:guava:18.0 +com.sun.xml.bind:jaxb-core:2.2.11 +com.sun.xml.bind:jaxb-impl:2.2.11 commons-codec:commons-codec:1.10 commons-logging:commons-logging:1.2 +javax.activation:activation:1.1.1 javax.annotation:javax.annotation-api:1.2 javax.validation:validation-api:1.1.0.Final javax.ws.rs:javax.ws.rs-api:2.1 +javax.xml.bind:jaxb-api:2.2.11 org.apache.commons:commons-lang3:3.4 org.apache.httpcomponents:httpclient:4.5.6 org.apache.httpcomponents:httpcore:4.4.10 From 4cca5637c05bc69749340b8b859b04c4b9b0a030 Mon Sep 17 00:00:00 2001 From: huylenq Date: Thu, 12 Nov 2020 13:10:55 +0700 Subject: [PATCH 08/38] Bump version to v0.2.11 --- CHANGELOG.md | 6 ++++++ build.gradle | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8203311..2d3c6e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.2.11 - 2020-11-12 + +* [enhancement] Apply Embulk Gradle plugin. +* [enhancement] Apply embulk-util-config for config loading. +* [enhancement] Upgrade Embulk to v0.10.19, along with necessary refactors. + ## 0.2.10 - 2020-03-18 * [enhancement] Use `java.util.Optional` instead of `com.google.common.base.Optional`, use `LoggerFactory.getLogger` instead of `Exec.getLogger` [#58](https://github.com/treasure-data/embulk-input-jira/pull/58) diff --git a/build.gradle b/build.gradle index 89e6600..db840b3 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ repositories { jcenter() } -version = "0.2.11.SNAPSHOT" +version = "0.2.11" group = "com.treasuredata.embulk.plugins" description = "JIRA Embulk input plugin." From adc2ae7b8423c29697d23468bd6d76e59f8623e5 Mon Sep 17 00:00:00 2001 From: NirmalaY12 <52509454+NirmalaY12@users.noreply.github.com> Date: Sun, 22 Nov 2020 23:18:40 -0800 Subject: [PATCH 09/38] Create codeql-analysis.yml Performs source code scan. --- .github/workflows/codeql-analysis.yml | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..38b1760 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,67 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '31 2 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 51dd3757d3865886898f36d65ebe80fdd233ad47 Mon Sep 17 00:00:00 2001 From: NirmalaY12 <52509454+NirmalaY12@users.noreply.github.com> Date: Sun, 22 Nov 2020 23:19:36 -0800 Subject: [PATCH 10/38] Update codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 38b1760..b1783fe 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,7 +13,7 @@ name: "CodeQL" on: push: - branches: [ master ] + branches: [ CodeQL ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] From 793817eabf29a3ac56f60b55570c31a78541c7ab Mon Sep 17 00:00:00 2001 From: NirmalaY12 <52509454+NirmalaY12@users.noreply.github.com> Date: Sun, 22 Nov 2020 23:23:32 -0800 Subject: [PATCH 11/38] Update codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b1783fe..ae00c5f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -12,11 +12,7 @@ name: "CodeQL" on: - push: - branches: [ CodeQL ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] + schedule: - cron: '31 2 * * 2' From cee2b1fa6c210c391f49c4d336b8778b9f38e527 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Sat, 21 Aug 2021 19:58:05 +0700 Subject: [PATCH 12/38] Modernized with Embulk v0.10.28 styles --- build.gradle | 29 +++--- .../embulkPluginRuntime.lockfile | 30 ------ lib/embulk/guess/jira.rb | 24 ----- lib/embulk/input/jira.rb | 3 - .../java/org/embulk/input/jira/Issue.java | 64 ++++++------ .../embulk/input/jira/JiraInputPlugin.java | 73 +++----------- .../org/embulk/input/jira/util/JiraUtil.java | 20 +++- .../input/jira/JiraInputPluginTest.java | 50 +++++----- .../input/jira/JiraPluginTestRuntime.java | 98 ------------------- .../org/embulk/input/jira/TestHelpers.java | 5 +- 10 files changed, 106 insertions(+), 290 deletions(-) delete mode 100644 gradle/dependency-locks/embulkPluginRuntime.lockfile delete mode 100644 lib/embulk/guess/jira.rb delete mode 100644 lib/embulk/input/jira.rb delete mode 100644 src/test/java/org/embulk/input/jira/JiraPluginTestRuntime.java diff --git a/build.gradle b/build.gradle index db840b3..295a999 100644 --- a/build.gradle +++ b/build.gradle @@ -18,34 +18,29 @@ description = "JIRA Embulk input plugin." sourceCompatibility = 1.8 targetCompatibility = 1.8 -def embulkVersion = '0.10.19' +def embulkVersion = '0.10.28' dependencies { compileOnly "org.embulk:embulk-api:$embulkVersion" compileOnly "org.embulk:embulk-spi:$embulkVersion" - // Required through GuessExecutor usage. - compileOnly "org.embulk:embulk-core:$embulkVersion" - - compile('org.embulk:embulk-util-config:0.1.4') { - // Conflict with Embulk Core + compile('org.embulk:embulk-util-config:0.3.1') { exclude group: 'com.fasterxml.jackson.core', module: 'jackson-annotations' exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind' exclude group: 'com.fasterxml.jackson.datatype', module: 'jackson-datatype-jdk8' exclude group: 'javax.validation', module: 'validation-api' } - compile('org.embulk:embulk-util-retryhelper:0.8.1') { - exclude group: 'org.slf4j', module: 'slf4j-api' - } + compile('org.embulk:embulk-util-retryhelper-jetty92:0.8.2') compile 'org.embulk:embulk-util-timestamp:0.2.1' + compile('org.embulk:embulk-util-guess:0.1.2') compile('org.embulk:embulk-util-json:0.1.0') { exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' exclude group: 'org.msgpack', module: 'msgpack-core' } - // Explicit dependencies for embulk-util-* that matches with Embulk 0.10.19 + // Explicit dependencies for embulk-util-* that matches with Embulk 0.10.28 compile 'com.fasterxml.jackson.core:jackson-core:2.6.7' compile 'com.fasterxml.jackson.core:jackson-annotations:2.6.7' compile 'com.fasterxml.jackson.core:jackson-databind:2.6.7' @@ -65,11 +60,19 @@ dependencies { compile 'com.google.guava:guava:18.0' compile 'org.apache.commons:commons-lang3:3.4' - testCompile "junit:junit:4.+" - testCompile "org.mockito:mockito-core:2.+" - testCompile "org.embulk:embulk-junit4:$embulkVersion" + testCompile "junit:junit:4.13" + testCompile("org.embulk:embulk-core:$embulkVersion") { + exclude group: 'com.fasterxml.jackson.core', module: 'jackson-annotations' + exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' + exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind' + exclude group: 'com.fasterxml.jackson.datatype', module: 'jackson-datatype-jdk8' + exclude group: 'javax.validation', module: 'validation-api' + } + testCompile "org.embulk:embulk-standards:$embulkVersion" testCompile "org.embulk:embulk-core:$embulkVersion:tests" testCompile "org.embulk:embulk-deps:$embulkVersion" + testCompile "org.embulk:embulk-junit4:$embulkVersion" + testCompile "org.mockito:mockito-core:2.28.2" } checkstyle { diff --git a/gradle/dependency-locks/embulkPluginRuntime.lockfile b/gradle/dependency-locks/embulkPluginRuntime.lockfile deleted file mode 100644 index c1debb5..0000000 --- a/gradle/dependency-locks/embulkPluginRuntime.lockfile +++ /dev/null @@ -1,30 +0,0 @@ -# This is a Gradle generated file for dependency locking. -# Manual edits can break the build and are not advised. -# This file is expected to be part of source control. -com.fasterxml.jackson.core:jackson-annotations:2.6.7 -com.fasterxml.jackson.core:jackson-core:2.6.7 -com.fasterxml.jackson.core:jackson-databind:2.6.7 -com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.6.7 -com.google.code.gson:gson:2.8.5 -com.google.guava:guava:18.0 -com.sun.xml.bind:jaxb-core:2.2.11 -com.sun.xml.bind:jaxb-impl:2.2.11 -commons-codec:commons-codec:1.10 -commons-logging:commons-logging:1.2 -javax.activation:activation:1.1.1 -javax.annotation:javax.annotation-api:1.2 -javax.validation:validation-api:1.1.0.Final -javax.ws.rs:javax.ws.rs-api:2.1 -javax.xml.bind:jaxb-api:2.2.11 -org.apache.commons:commons-lang3:3.4 -org.apache.httpcomponents:httpclient:4.5.6 -org.apache.httpcomponents:httpcore:4.4.10 -org.embulk:embulk-util-config:0.1.4 -org.embulk:embulk-util-json:0.1.0 -org.embulk:embulk-util-retryhelper:0.8.1 -org.embulk:embulk-util-rubytime:0.3.2 -org.embulk:embulk-util-timestamp:0.2.1 -org.glassfish.hk2.external:javax.inject:2.5.0-b42 -org.glassfish.hk2:osgi-resource-locator:1.0.1 -org.glassfish.jersey.core:jersey-client:2.27 -org.glassfish.jersey.core:jersey-common:2.27 diff --git a/lib/embulk/guess/jira.rb b/lib/embulk/guess/jira.rb deleted file mode 100644 index 449720a..0000000 --- a/lib/embulk/guess/jira.rb +++ /dev/null @@ -1,24 +0,0 @@ -# Not to be used as a standalone guess plugin -# This is just a thin wrapper to leverage the SchemaGuess Ruby implementation from Java side - -require 'json' - -module Embulk - module Guess - class JiraGuess < TextGuessPlugin - Plugin.register_guess("jira", self) - - def guess_text(config, sample_text) - {:columns => - SchemaGuess.from_hash_records(JSON.parse(sample_text)).map do |c| - { - name: c.name, - type: c.type, - **(c.format ? {format: c.format} : {}) - } - end - } - end - end - end -end \ No newline at end of file diff --git a/lib/embulk/input/jira.rb b/lib/embulk/input/jira.rb deleted file mode 100644 index 47555c7..0000000 --- a/lib/embulk/input/jira.rb +++ /dev/null @@ -1,3 +0,0 @@ -Embulk::JavaPlugin.register_input( - "jira", "org.embulk.input.jira.JiraInputPlugin", - File.expand_path('../../../../classpath', __FILE__)) diff --git a/src/main/java/org/embulk/input/jira/Issue.java b/src/main/java/org/embulk/input/jira/Issue.java index 7445ee1..01b48a2 100644 --- a/src/main/java/org/embulk/input/jira/Issue.java +++ b/src/main/java/org/embulk/input/jira/Issue.java @@ -18,20 +18,20 @@ public class Issue { private JsonObject flatten; - private JsonObject json; + private final JsonObject json; - public Issue(JsonObject original) + public Issue(final JsonObject original) { this.json = original; } - public JsonElement getValue(String path) + public JsonElement getValue(final String path) { - List keys = new ArrayList<>(Arrays.asList(path.split("\\."))); + final List keys = new ArrayList<>(Arrays.asList(path.split("\\."))); return get(json, keys); } - private JsonElement get(JsonElement json, List keys) + private JsonElement get(final JsonElement json, final List keys) { if (json == null || json.isJsonNull()) { return JsonNull.INSTANCE; @@ -39,11 +39,11 @@ private JsonElement get(JsonElement json, List keys) else if (keys.isEmpty() || (json.isJsonArray() && json.getAsJsonArray().size() == 0)) { return json; } - String key = keys.get(0); + final String key = keys.get(0); keys.remove(0); if (json.isJsonArray()) { - JsonArray arrays = new JsonArray(); - for (JsonElement elem : json.getAsJsonArray()) { + final JsonArray arrays = new JsonArray(); + for (final JsonElement elem : json.getAsJsonArray()) { if (elem.isJsonObject()) { arrays.add(elem.getAsJsonObject().get(key)); } @@ -67,10 +67,10 @@ public synchronized JsonObject getFlatten() return flatten; } - private void manipulatingFlattenJson(JsonElement in, String prefix) + private void manipulatingFlattenJson(final JsonElement in, final String prefix) { if (in.isJsonObject()) { - JsonObject obj = in.getAsJsonObject(); + final JsonObject obj = in.getAsJsonObject(); // NOTE: If you want to flatten JSON completely, please remove this if and addHeuristicValue if (StringUtils.countMatches(prefix, ".") > 1) { addHeuristicValue(obj, prefix); @@ -80,29 +80,29 @@ private void manipulatingFlattenJson(JsonElement in, String prefix) flatten.add(prefix, obj); } else { - for (Entry entry : obj.entrySet()) { - String key = entry.getKey(); - JsonElement value = entry.getValue(); + for (final Entry entry : obj.entrySet()) { + final String key = entry.getKey(); + final JsonElement value = entry.getValue(); manipulatingFlattenJson(value, appendPrefix(prefix, key)); } } } else if (in.isJsonArray()) { - JsonArray arrayObj = in.getAsJsonArray(); - boolean isAllJsonObject = arrayObj.size() > 0 && StreamSupport.stream(arrayObj.spliterator(), false).allMatch(JsonElement::isJsonObject); + final JsonArray arrayObj = in.getAsJsonArray(); + final boolean isAllJsonObject = arrayObj.size() > 0 && StreamSupport.stream(arrayObj.spliterator(), false).allMatch(JsonElement::isJsonObject); if (isAllJsonObject) { - Map occurents = new HashMap<>(); - for (JsonElement element : arrayObj) { - JsonObject obj = element.getAsJsonObject(); - for (Entry entry : obj.entrySet()) { - String key = entry.getKey(); + final Map occurents = new HashMap<>(); + for (final JsonElement element : arrayObj) { + final JsonObject obj = element.getAsJsonObject(); + for (final Entry entry : obj.entrySet()) { + final String key = entry.getKey(); occurents.merge(key, 1, Integer::sum); } } - JsonObject newObj = new JsonObject(); - for (String key : occurents.keySet()) { + final JsonObject newObj = new JsonObject(); + for (final String key : occurents.keySet()) { newObj.add(key, new JsonArray()); - for (JsonElement elem : arrayObj) { + for (final JsonElement elem : arrayObj) { newObj.get(key).getAsJsonArray().add(elem.getAsJsonObject().get(key)); } } @@ -121,13 +121,13 @@ else if (in.isJsonPrimitive()) { } } - private void addHeuristicValue(JsonObject json, String prefix) + private void addHeuristicValue(final JsonObject json, final String prefix) { - List keys = Arrays.asList("name", "key", "id"); - List heuristic = new ArrayList<>(); - for (Entry entry : json.entrySet()) { - String key = entry.getKey(); - JsonElement value = entry.getValue(); + final List keys = Arrays.asList("name", "key", "id"); + final List heuristic = new ArrayList<>(); + for (final Entry entry : json.entrySet()) { + final String key = entry.getKey(); + final JsonElement value = entry.getValue(); if (keys.contains(key) && !value.isJsonNull()) { heuristic.add(key); } @@ -136,14 +136,14 @@ private void addHeuristicValue(JsonObject json, String prefix) flatten.add(prefix, new JsonPrimitive(json.toString())); } else { - for (String key : heuristic) { - JsonElement value = json.get(key); + for (final String key : heuristic) { + final JsonElement value = json.get(key); flatten.add(appendPrefix(prefix, key), value); } } } - private String appendPrefix(String prefix, String key) + private String appendPrefix(final String prefix, final String key) { return prefix.isEmpty() ? key : prefix + "." + key; } diff --git a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java index 7ab8e01..9634323 100644 --- a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java +++ b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java @@ -1,10 +1,7 @@ package org.embulk.input.jira; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; @@ -13,18 +10,13 @@ import org.embulk.config.ConfigSource; import org.embulk.config.TaskReport; import org.embulk.config.TaskSource; -import org.embulk.exec.GuessExecutor; import org.embulk.input.jira.client.JiraClient; import org.embulk.input.jira.util.JiraUtil; -import org.embulk.spi.Buffer; -import org.embulk.spi.BufferAllocator; import org.embulk.spi.Exec; -import org.embulk.spi.ExecInternal; import org.embulk.spi.InputPlugin; import org.embulk.spi.PageBuilder; import org.embulk.spi.PageOutput; import org.embulk.spi.Schema; -import org.embulk.spi.SchemaConfig; import org.embulk.util.config.Config; import org.embulk.util.config.ConfigDefault; import org.embulk.util.config.ConfigMapper; @@ -32,9 +24,13 @@ import org.embulk.util.config.Task; import org.embulk.util.config.TaskMapper; import org.embulk.util.config.modules.TypeModule; +import org.embulk.util.config.units.SchemaConfig; +import org.embulk.util.guess.SchemaGuess; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map.Entry; import java.util.Optional; @@ -42,7 +38,6 @@ import java.util.SortedSet; import java.util.TreeSet; -import static org.embulk.input.jira.Constant.GUESS_BUFFER_SIZE; import static org.embulk.input.jira.Constant.GUESS_RECORDS_COUNT; import static org.embulk.input.jira.Constant.MAX_RESULTS; import static org.embulk.input.jira.Constant.PREVIEW_RECORDS_COUNT; @@ -124,13 +119,6 @@ public ConfigDiff resume(final TaskSource taskSource, return CONFIG_MAPPER_FACTORY.newConfigDiff(); } - @Override - public void cleanup(final TaskSource taskSource, - final Schema schema, final int taskCount, - final List successTaskReports) - { - } - @Override public TaskReport run(final TaskSource taskSource, final Schema schema, final int taskIndex, @@ -175,24 +163,11 @@ public ConfigDiff guess(final ConfigSource config) if (issues.isEmpty()) { throw new ConfigException("Could not guess schema due to empty data set"); } - - // TODO: Eliminate the use of ExecInternal.getInjector that soon will be obsolete. - final Buffer sample = Buffer.copyOf(createSamples(issues, getUniqueAttributes(issues)).toString().getBytes()); - final JsonNode columns = ExecInternal.getInjector().getInstance(GuessExecutor.class) - .guessParserConfig(sample, Exec.newConfigSource(), createGuessConfig()) - .get(JsonNode.class, "columns"); - + final List columns = SchemaGuess.of(CONFIG_MAPPER_FACTORY).fromLinkedHashMapRecords(createGuessSample(issues, getUniqueAttributes(issues))); + columns.forEach(conf -> conf.remove("index")); return CONFIG_MAPPER_FACTORY.newConfigDiff().set("columns", columns); } - private ConfigSource createGuessConfig() - { - // TODO: there will be no need for this after "Eliminate the use of ExecInternal.getInjector that soon will be obsolete." - return Exec.newConfigSource() - .set("guess_plugins", ImmutableList.of("jira")) - .set("guess_sample_buffer_bytes", GUESS_BUFFER_SIZE); - } - private SortedSet getUniqueAttributes(final List issues) { final SortedSet uniqueAttributes = new TreeSet<>(); @@ -204,9 +179,9 @@ private SortedSet getUniqueAttributes(final List issues) return uniqueAttributes; } - private JsonArray createSamples(final List issues, final Set uniqueAttributes) + private List> createGuessSample(final List issues, final Set uniqueAttributes) { - final JsonArray samples = new JsonArray(); + final List> samples = new ArrayList<>(); for (final Issue issue : issues) { final JsonObject flatten = issue.getFlatten(); final JsonObject unified = new JsonObject(); @@ -217,28 +192,15 @@ private JsonArray createSamples(final List issues, final Set uniq } unified.add(key, value); } - samples.add(unified); + samples.add(JiraUtil.toLinkedHashMap(unified)); } return samples; } - @VisibleForTesting - public GuessExecutor getGuessExecutor() - { - return ExecInternal.getInjector().getInstance(GuessExecutor.class); - } - - @SuppressWarnings("deprecation") @VisibleForTesting public PageBuilder getPageBuilder(final Schema schema, final PageOutput output) { - if (HAS_EXEC_GET_PAGE_BUILDER) { - return Exec.getPageBuilder(Exec.getBufferAllocator(), schema, output); - } - else { - // To be compatible to run on Embulk belows v0.10.17 - return new PageBuilder(Exec.getBufferAllocator(), schema, output); - } + return Exec.getPageBuilder(Exec.getBufferAllocator(), schema, output); } @VisibleForTesting @@ -252,16 +214,7 @@ public JiraClient getJiraClient() { return new JiraClient(); } - - private static boolean hasExecGetPageBuilder() - { - try { - Exec.class.getMethod("getPageBuilder", BufferAllocator.class, Schema.class, PageOutput.class); - } - catch (final NoSuchMethodException ex) { - return false; - } - return true; - } - private static final boolean HAS_EXEC_GET_PAGE_BUILDER = hasExecGetPageBuilder(); + @Override + public void cleanup(final TaskSource taskSource, final Schema schema, final int taskCount, final List successTaskReports) + {} } diff --git a/src/main/java/org/embulk/input/jira/util/JiraUtil.java b/src/main/java/org/embulk/input/jira/util/JiraUtil.java index 7fa4a1d..d25a1af 100644 --- a/src/main/java/org/embulk/input/jira/util/JiraUtil.java +++ b/src/main/java/org/embulk/input/jira/util/JiraUtil.java @@ -1,6 +1,8 @@ package org.embulk.input.jira.util; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; @@ -12,10 +14,10 @@ import org.embulk.input.jira.Issue; import org.embulk.input.jira.JiraInputPlugin.PluginTask; import org.embulk.spi.Column; -import org.embulk.spi.ColumnConfig; import org.embulk.spi.ColumnVisitor; import org.embulk.spi.PageBuilder; import org.embulk.spi.Schema; +import org.embulk.util.config.units.ColumnConfig; import org.embulk.util.json.JsonParser; import org.embulk.util.timestamp.TimestampFormatter; @@ -23,6 +25,7 @@ import java.io.IOException; import java.time.Instant; +import java.util.LinkedHashMap; import java.util.List; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -265,4 +268,19 @@ public void doubleColumn(final Column column) }); pageBuilder.addRecord(); } + + public static LinkedHashMap toLinkedHashMap(final JsonObject flt) + { + final LinkedHashMap result = new LinkedHashMap<>(); + for (final String key : flt.keySet()) { + final JsonElement elem = flt.get(key); + if (elem.isJsonPrimitive()) { + result.put(key, flt.get(key).getAsString()); + } + else { + result.put(key, elem); + } + } + return result; + } } diff --git a/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java b/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java index 3380906..c5a5d10 100644 --- a/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java +++ b/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java @@ -8,6 +8,7 @@ import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; +import org.embulk.EmbulkTestRuntime; import org.embulk.config.ConfigDiff; import org.embulk.config.ConfigSource; import org.embulk.config.TaskReport; @@ -37,17 +38,17 @@ public class JiraInputPluginTest { @Rule - public JiraPluginTestRuntime runtime = new JiraPluginTestRuntime(); + public EmbulkTestRuntime runtime = new EmbulkTestRuntime(); private JiraInputPlugin plugin; private JiraClient jiraClient; private JsonObject data; private ConfigSource config; - private CloseableHttpClient client = Mockito.mock(CloseableHttpClient.class); - private CloseableHttpResponse response = Mockito.mock(CloseableHttpResponse.class); - private StatusLine statusLine = Mockito.mock(StatusLine.class); + private final CloseableHttpClient client = Mockito.mock(CloseableHttpClient.class); + private final CloseableHttpResponse response = Mockito.mock(CloseableHttpResponse.class); + private final StatusLine statusLine = Mockito.mock(StatusLine.class); - private MockPageOutput output = new MockPageOutput(); + private final MockPageOutput output = new MockPageOutput(); private PageBuilder pageBuilder; @Before @@ -71,8 +72,8 @@ public void setUp() throws IOException @Test public void test_run_withEmptyResult() throws IOException { - JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); - JsonObject searchResponse = data.get("emptyResult").getAsJsonObject(); + final JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); + final JsonObject searchResponse = data.get("emptyResult").getAsJsonObject(); when(statusLine.getStatusCode()) .thenReturn(authorizeResponse.get("statusCode").getAsInt()) @@ -91,8 +92,8 @@ public void test_run_withEmptyResult() throws IOException @Test public void test_run_with1RecordsResult() throws IOException { - JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); - JsonObject searchResponse = data.get("oneRecordResult").getAsJsonObject(); + final JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); + final JsonObject searchResponse = data.get("oneRecordResult").getAsJsonObject(); when(statusLine.getStatusCode()) .thenReturn(authorizeResponse.get("statusCode").getAsInt()) @@ -111,8 +112,8 @@ public void test_run_with1RecordsResult() throws IOException @Test public void test_run_with2PagesResult() throws IOException { - JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); - JsonObject searchResponse = data.get("2PagesResult").getAsJsonObject(); + final JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); + final JsonObject searchResponse = data.get("2PagesResult").getAsJsonObject(); when(statusLine.getStatusCode()) .thenReturn(authorizeResponse.get("statusCode").getAsInt()) @@ -132,8 +133,8 @@ public void test_run_with2PagesResult() throws IOException public void test_preview_withEmptyResult() throws IOException { when(plugin.isPreview()).thenReturn(true); - JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); - JsonObject searchResponse = data.get("emptyResult").getAsJsonObject(); + final JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); + final JsonObject searchResponse = data.get("emptyResult").getAsJsonObject(); when(statusLine.getStatusCode()) .thenReturn(authorizeResponse.get("statusCode").getAsInt()) @@ -152,8 +153,8 @@ public void test_preview_withEmptyResult() throws IOException public void test_preview_with1RecordsResult() throws IOException { when(plugin.isPreview()).thenReturn(true); - JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); - JsonObject searchResponse = data.get("oneRecordResult").getAsJsonObject(); + final JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); + final JsonObject searchResponse = data.get("oneRecordResult").getAsJsonObject(); when(statusLine.getStatusCode()) .thenReturn(authorizeResponse.get("statusCode").getAsInt()) @@ -172,15 +173,10 @@ public void test_preview_with1RecordsResult() throws IOException @Test public void test_guess() throws IOException { - // ConfigSource configSource = TestHelpers.config( - // new ConfigLoader( - // // TODO: What about TypeJacksonModule? - // new ModelManager(null, new ObjectMapper().registerModule(new Jdk8Module())) - // ).newConfigSource()); - ConfigSource configSource = TestHelpers.config(); + final ConfigSource configSource = TestHelpers.config(); - JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); - JsonObject searchResponse = data.get("guessDataResult").getAsJsonObject(); + final JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); + final JsonObject searchResponse = data.get("guessDataResult").getAsJsonObject(); when(statusLine.getStatusCode()) .thenReturn(authorizeResponse.get("statusCode").getAsInt()) @@ -189,9 +185,9 @@ public void test_guess() throws IOException .thenReturn(new StringEntity(searchResponse.get("body").toString())) .thenReturn(new StringEntity(searchResponse.get("body").toString())); - ConfigDiff result = plugin.guess(configSource); - JsonElement expected = data.get("guessResult").getAsJsonObject(); - JsonElement actual = new JsonParser().parse(result.toString()); + final ConfigDiff result = plugin.guess(configSource); + final JsonElement expected = data.get("guessResult").getAsJsonObject(); + final JsonElement actual = new JsonParser().parse(result.toString()); assertEquals(expected, actual); } @@ -200,7 +196,7 @@ private class Control implements InputPlugin.Control @Override public List run(final TaskSource taskSource, final Schema schema, final int taskCount) { - List reports = new ArrayList<>(); + final List reports = new ArrayList<>(); for (int i = 0; i < taskCount; i++) { reports.add(plugin.run(taskSource, schema, i, output)); } diff --git a/src/test/java/org/embulk/input/jira/JiraPluginTestRuntime.java b/src/test/java/org/embulk/input/jira/JiraPluginTestRuntime.java deleted file mode 100644 index 5114c14..0000000 --- a/src/test/java/org/embulk/input/jira/JiraPluginTestRuntime.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.embulk.input.jira; - -import com.google.inject.Binder; -import com.google.inject.Injector; -import com.google.inject.Module; -import org.embulk.EmbulkSystemProperties; -import org.embulk.GuiceBinder; -import org.embulk.TestPluginSourceModule; -import org.embulk.TestUtilityModule; -import org.embulk.config.ConfigSource; -import org.embulk.config.DataSourceImpl; -import org.embulk.config.ModelManager; -import org.embulk.exec.ExecModule; -import org.embulk.exec.ExtensionServiceLoaderModule; -import org.embulk.exec.SystemConfigModule; -import org.embulk.jruby.JRubyScriptingModule; -import org.embulk.spi.ExecAction; -import org.embulk.spi.ExecInternal; -import org.embulk.spi.ExecSessionInternal; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -import java.util.Properties; - -/** - * This is a clone from {@link org.embulk.EmbulkTestRuntime}, since there is no easy way to extend it. - * The sole modification is the providing of "jruby_load_path" system property. - * This will enable tests to able to locate for guess plugins. - */ -public class JiraPluginTestRuntime extends GuiceBinder -{ - public static class TestRuntimeModule implements Module - { - @Override - public void configure(Binder binder) - { - final EmbulkSystemProperties embulkSystemProperties = EmbulkSystemProperties.of(new Properties() - { - { - setProperty("jruby_load_path", "lib"); - } - }); - new SystemConfigModule(embulkSystemProperties).configure(binder); - new ExecModule(embulkSystemProperties).configure(binder); - new ExtensionServiceLoaderModule(embulkSystemProperties).configure(binder); - new JRubyScriptingModule().configure(binder); - new TestUtilityModule().configure(binder); - new TestPluginSourceModule().configure(binder); - } - } - - private ExecSessionInternal exec; - - public JiraPluginTestRuntime() - { - super(new JiraPluginTestRuntime.TestRuntimeModule()); - Injector injector = getInjector(); - ConfigSource execConfig = new DataSourceImpl(injector.getInstance(ModelManager.class)); - this.exec = ExecSessionInternal.builderInternal(injector).fromExecConfig(execConfig).build(); - } - - @Override - public Statement apply(Statement base, Description description) - { - final Statement superStatement = JiraPluginTestRuntime.super.apply(base, description); - return new Statement() - { - public void evaluate() throws Throwable - { - try { - ExecInternal.doWith(exec, (ExecAction) () -> { - try { - superStatement.evaluate(); - } - catch (Throwable ex) { - throw new RuntimeExecutionException(ex); - } - return null; - }); - } - catch (RuntimeException ex) { - throw ex.getCause(); - } - finally { - exec.cleanup(); - } - } - }; - } - - private static class RuntimeExecutionException extends RuntimeException - { - public RuntimeExecutionException(Throwable cause) - { - super(cause); - } - } -} diff --git a/src/test/java/org/embulk/input/jira/TestHelpers.java b/src/test/java/org/embulk/input/jira/TestHelpers.java index 7bb0b03..c2525ef 100644 --- a/src/test/java/org/embulk/input/jira/TestHelpers.java +++ b/src/test/java/org/embulk/input/jira/TestHelpers.java @@ -5,9 +5,9 @@ import com.google.gson.JsonParser; import com.google.gson.stream.JsonReader; import org.embulk.config.ConfigSource; -import org.embulk.spi.ColumnConfig; -import org.embulk.spi.SchemaConfig; import org.embulk.spi.type.Types; +import org.embulk.util.config.units.ColumnConfig; +import org.embulk.util.config.units.SchemaConfig; import java.io.FileReader; import java.io.IOException; @@ -30,6 +30,7 @@ public static JsonObject getJsonFromFile(final String fileName) throws IOExcepti } } + @SuppressWarnings("serial") public static ConfigSource config() { return CONFIG_MAPPER_FACTORY.newConfigSource() From 2e0519a23ab029ed41328e1fbadd02c36a01abe0 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Sat, 21 Aug 2021 20:00:47 +0700 Subject: [PATCH 13/38] Update gem version, readme and changelog --- CHANGELOG.md | 5 +++++ README.md | 2 +- build.gradle | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d3c6e9..f50004d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.2.12 - 2021-08-24 +* [enhancement] Mordernized with embulk v0.10.x styles +* [enhancement] Use embulk-guess-util +* [enhancement] Remove internal jruby guess plugin + ## 0.2.11 - 2020-11-12 * [enhancement] Apply Embulk Gradle plugin. diff --git a/README.md b/README.md index ddadf8a..8126a17 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ embulk-input-jira is the Embulk input plugin for [JIRA](https://www.atlassian.co ## Overview -Required Embulk version >= 0.9.20 +Required Embulk version >= 0.10.19 * **Plugin type**: input * **Resume supported**: no diff --git a/build.gradle b/build.gradle index 295a999..8dd946d 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ repositories { jcenter() } -version = "0.2.11" +version = "0.2.12" group = "com.treasuredata.embulk.plugins" description = "JIRA Embulk input plugin." From e588a127d812b0415c1e9e47128b64edcbaa32fe Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Mon, 23 Aug 2021 09:29:06 +0700 Subject: [PATCH 14/38] Remove legacy ruby embeeded inside gem --- build.gradle | 11 ++--------- .../java/org/embulk/input/jira/JiraInputPlugin.java | 3 --- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index 8dd946d..5847e3a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,10 +2,11 @@ plugins { id "java" id "checkstyle" id "jacoco" + id "org.embulk.embulk-plugins" version "0.4.2" id "maven" id 'maven-publish' - id "org.embulk.embulk-plugins" version "0.4.2" } + repositories { mavenCentral() jcenter() @@ -113,14 +114,6 @@ gem { summary = project.description homepage = 'https://github.com/treasure-data/embulk-input-jira' licenses = ['Apache-2.0'] - - generateRubyCode = false - into("lib/embulk/input/") { - from "lib/embulk/input/jira.rb" - } - into("lib/embulk/guess/") { - from "lib/embulk/guess/jira.rb" - } } gemPush { diff --git a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java index 9634323..4c4b52b 100644 --- a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java +++ b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java @@ -23,7 +23,6 @@ import org.embulk.util.config.ConfigMapperFactory; import org.embulk.util.config.Task; import org.embulk.util.config.TaskMapper; -import org.embulk.util.config.modules.TypeModule; import org.embulk.util.config.units.SchemaConfig; import org.embulk.util.guess.SchemaGuess; import org.slf4j.Logger; @@ -50,8 +49,6 @@ public class JiraInputPlugin public static final ConfigMapperFactory CONFIG_MAPPER_FACTORY = ConfigMapperFactory .builder() .addDefaultModules() - // required for PluginTask > SchemaConfig > ColumnConfig > Type - .addModule(new TypeModule()) .build(); @VisibleForTesting public static final ConfigMapper CONFIG_MAPPER = CONFIG_MAPPER_FACTORY.createConfigMapper(); From 2579e9d65ef022c6bb2ff089bb86dba477868139 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Mon, 23 Aug 2021 13:58:05 +0700 Subject: [PATCH 15/38] Add dependencies lock --- .../embulkPluginRuntime.lockfile | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 gradle/dependency-locks/embulkPluginRuntime.lockfile diff --git a/gradle/dependency-locks/embulkPluginRuntime.lockfile b/gradle/dependency-locks/embulkPluginRuntime.lockfile new file mode 100644 index 0000000..086251a --- /dev/null +++ b/gradle/dependency-locks/embulkPluginRuntime.lockfile @@ -0,0 +1,39 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.fasterxml.jackson.core:jackson-annotations:2.6.7 +com.fasterxml.jackson.core:jackson-core:2.6.7 +com.fasterxml.jackson.core:jackson-databind:2.6.7 +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.6.7 +com.google.code.gson:gson:2.8.5 +com.google.guava:guava:18.0 +com.ibm.icu:icu4j:54.1.1 +com.sun.xml.bind:jaxb-core:2.2.11 +com.sun.xml.bind:jaxb-impl:2.2.11 +commons-codec:commons-codec:1.10 +commons-logging:commons-logging:1.2 +javax.activation:activation:1.1.1 +javax.annotation:javax.annotation-api:1.2 +javax.validation:validation-api:1.1.0.Final +javax.ws.rs:javax.ws.rs-api:2.1 +javax.xml.bind:jaxb-api:2.2.11 +org.apache.commons:commons-lang3:3.4 +org.apache.httpcomponents:httpclient:4.5.6 +org.apache.httpcomponents:httpcore:4.4.10 +org.eclipse.jetty:jetty-client:9.2.14.v20151106 +org.eclipse.jetty:jetty-http:9.2.14.v20151106 +org.eclipse.jetty:jetty-io:9.2.14.v20151106 +org.eclipse.jetty:jetty-util:9.2.14.v20151106 +org.embulk:embulk-util-config:0.3.1 +org.embulk:embulk-util-file:0.1.1 +org.embulk:embulk-util-guess:0.1.2 +org.embulk:embulk-util-json:0.1.0 +org.embulk:embulk-util-retryhelper-jetty92:0.8.2 +org.embulk:embulk-util-retryhelper:0.8.2 +org.embulk:embulk-util-rubytime:0.3.2 +org.embulk:embulk-util-text:0.1.0 +org.embulk:embulk-util-timestamp:0.2.1 +org.glassfish.hk2.external:javax.inject:2.5.0-b42 +org.glassfish.hk2:osgi-resource-locator:1.0.1 +org.glassfish.jersey.core:jersey-client:2.27 +org.glassfish.jersey.core:jersey-common:2.27 From 6010fc8b63c6f373fdbf1c20d94e111dc28bafb5 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Thu, 23 Sep 2021 14:55:03 +0700 Subject: [PATCH 16/38] Support dynamic schema ingestion --- .../embulk/input/jira/JiraInputPlugin.java | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java index 4c4b52b..3b7e224 100644 --- a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java +++ b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java @@ -23,6 +23,7 @@ import org.embulk.util.config.ConfigMapperFactory; import org.embulk.util.config.Task; import org.embulk.util.config.TaskMapper; +import org.embulk.util.config.units.ColumnConfig; import org.embulk.util.config.units.SchemaConfig; import org.embulk.util.guess.SchemaGuess; import org.slf4j.Logger; @@ -86,6 +87,10 @@ public interface PluginTask @ConfigDefault("null") public Optional getJQL(); + @Config("dynamic_schema") + @ConfigDefault("false") + public boolean getDynamicSchema(); + @Config("columns") public SchemaConfig getColumns(); @@ -100,8 +105,17 @@ public ConfigDiff transaction(final ConfigSource config, final InputPlugin.Control control) { final PluginTask task = CONFIG_MAPPER.map(config, PluginTask.class); - - final Schema schema = task.getColumns().toSchema(); + SchemaConfig schemaConfig = task.getColumns(); + if (task.getDynamicSchema()) { + final JiraClient jiraClient = getJiraClient(); + final List columns = new ArrayList<>(); + final List guessedColumns = getGuessedColumns(jiraClient, task); + for (final ConfigDiff guessedColumn : guessedColumns) { + columns.add(new ColumnConfig(CONFIG_MAPPER_FACTORY.newConfigSource().merge(guessedColumn))); + } + schemaConfig = new SchemaConfig(columns); + } + final Schema schema = schemaConfig.toSchema(); final int taskCount = 1; return resume(task.toTaskSource(), schema, taskCount, control); @@ -156,13 +170,18 @@ public ConfigDiff guess(final ConfigSource config) JiraUtil.validateTaskConfig(task); final JiraClient jiraClient = getJiraClient(); jiraClient.checkUserCredentials(task); + return CONFIG_MAPPER_FACTORY.newConfigDiff().set("columns", getGuessedColumns(jiraClient, task)); + } + + private List getGuessedColumns(final JiraClient jiraClient, final PluginTask task) + { final List issues = jiraClient.searchIssues(task, 0, GUESS_RECORDS_COUNT); if (issues.isEmpty()) { throw new ConfigException("Could not guess schema due to empty data set"); } final List columns = SchemaGuess.of(CONFIG_MAPPER_FACTORY).fromLinkedHashMapRecords(createGuessSample(issues, getUniqueAttributes(issues))); columns.forEach(conf -> conf.remove("index")); - return CONFIG_MAPPER_FACTORY.newConfigDiff().set("columns", columns); + return columns; } private SortedSet getUniqueAttributes(final List issues) From fde88f32afe478e53821370b91791072141dca48 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Thu, 23 Sep 2021 15:16:45 +0700 Subject: [PATCH 17/38] Add test for dynamic schema feature --- .../input/jira/JiraInputPluginTest.java | 36 +++++++++++++++++++ .../org/embulk/input/jira/TestHelpers.java | 5 +++ 2 files changed, 41 insertions(+) diff --git a/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java b/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java index c5a5d10..f3dc5bd 100644 --- a/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java +++ b/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java @@ -10,6 +10,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.embulk.EmbulkTestRuntime; import org.embulk.config.ConfigDiff; +import org.embulk.config.ConfigException; import org.embulk.config.ConfigSource; import org.embulk.config.TaskReport; import org.embulk.config.TaskSource; @@ -89,6 +90,41 @@ public void test_run_withEmptyResult() throws IOException verify(pageBuilder, times(1)).finish(); } + @Test(expected = ConfigException.class) + public void test_runDynamicSchema_withEmptyResult() throws IOException + { + final JsonObject searchResponse = data.get("emptyResult").getAsJsonObject(); + + when(statusLine.getStatusCode()) + .thenReturn(searchResponse.get("statusCode").getAsInt()); + when(response.getEntity()) + .thenReturn(new StringEntity(searchResponse.get("body").toString())); + + plugin.transaction(TestHelpers.dynamicSchemaConfig(), new Control()); + } + + @Test + public void test_runDynamicSchema_withResult() throws IOException + { + final JsonObject authorizeResponse = data.get("authenticateSuccess").getAsJsonObject(); + final JsonObject searchResponse = data.get("oneRecordResult").getAsJsonObject(); + + when(statusLine.getStatusCode()) + .thenReturn(searchResponse.get("statusCode").getAsInt()) + .thenReturn(authorizeResponse.get("statusCode").getAsInt()) + .thenReturn(searchResponse.get("statusCode").getAsInt()); + when(response.getEntity()) + .thenReturn(new StringEntity(searchResponse.get("body").toString())) + .thenReturn(new StringEntity(authorizeResponse.get("body").toString())) + .thenReturn(new StringEntity(searchResponse.get("body").toString())); + + plugin.transaction(TestHelpers.dynamicSchemaConfig(), new Control()); + // Check credential 1 + getTotal 1 + loadData 2 + verify(jiraClient, times(4)).createHttpClient(); + verify(pageBuilder, times(1)).addRecord(); + verify(pageBuilder, times(1)).finish(); + } + @Test public void test_run_with1RecordsResult() throws IOException { diff --git a/src/test/java/org/embulk/input/jira/TestHelpers.java b/src/test/java/org/embulk/input/jira/TestHelpers.java index c2525ef..807bdc7 100644 --- a/src/test/java/org/embulk/input/jira/TestHelpers.java +++ b/src/test/java/org/embulk/input/jira/TestHelpers.java @@ -55,5 +55,10 @@ public static ConfigSource config() })); } + public static ConfigSource dynamicSchemaConfig() + { + return config().set("dynamic_schema", true); + } + private static final ConfigSource EMPTY_CONFIG_SOURCE = CONFIG_MAPPER_FACTORY.newConfigSource(); } From 4eec18cddcc4a1d4c9ab4f36ea2f609d264d0b4a Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Thu, 23 Sep 2021 15:22:20 +0700 Subject: [PATCH 18/38] Update gem version, readme and changelog --- CHANGELOG.md | 4 ++++ README.md | 7 +++++++ build.gradle | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f50004d..fc231ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.13 - 2021-09-30 +* [enhancement] Support dynamic schema +* PR [#69](https://github.com/treasure-data/embulk-input-jira/pull/69) + ## 0.2.12 - 2021-08-24 * [enhancement] Mordernized with embulk v0.10.x styles * [enhancement] Use embulk-guess-util diff --git a/README.md b/README.md index 8126a17..5307680 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Required Embulk version >= 0.10.19 - **password** JIRA password or API keys (string, required) - **uri** JIRA API endpoint (string, required) - **jql** [JQL](https://confluence.atlassian.com/display/JIRA/Advanced+Searching) for extract target issues (string, required) +- **dynamic_schema** Used it to refresh the schema each time ingestion (boolean, default: `false`) - **columns** target issue attributes. You can generate this configuration by `guess` command (array, required) - **retry_initial_wait_sec**: Wait seconds for exponential backoff initial value (integer, default: 1) - **retry_limit**: Try to retry this times (integer, default: 5) @@ -48,4 +49,10 @@ in: ``` $ ./gradlew gem # -t to watch change of files and rebuild continuously +``` + +## Test + +``` +$ ./gradlew checkstyle test jacocoTestReport ``` \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5847e3a..8770524 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ repositories { jcenter() } -version = "0.2.12" +version = "0.2.13" group = "com.treasuredata.embulk.plugins" description = "JIRA Embulk input plugin." From 4c10228497fb2f75bdb51f3e2191df48bd571c46 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Mon, 4 Oct 2021 10:21:56 +0700 Subject: [PATCH 19/38] Graceful shutdown in case no records for dynamic schema --- .../java/org/embulk/input/jira/JiraInputPlugin.java | 13 ++++++++++--- .../org/embulk/input/jira/JiraInputPluginTest.java | 4 +--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java index 3b7e224..f7464b1 100644 --- a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java +++ b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java @@ -109,9 +109,16 @@ public ConfigDiff transaction(final ConfigSource config, if (task.getDynamicSchema()) { final JiraClient jiraClient = getJiraClient(); final List columns = new ArrayList<>(); - final List guessedColumns = getGuessedColumns(jiraClient, task); - for (final ConfigDiff guessedColumn : guessedColumns) { - columns.add(new ColumnConfig(CONFIG_MAPPER_FACTORY.newConfigSource().merge(guessedColumn))); + try { + final List guessedColumns = getGuessedColumns(jiraClient, task); + for (final ConfigDiff guessedColumn : guessedColumns) { + columns.add(new ColumnConfig(CONFIG_MAPPER_FACTORY.newConfigSource().merge(guessedColumn))); + } + } + catch (final ConfigException e) { + if (!e.getMessage().equals("Could not guess schema due to empty data set")) { + throw e; + } } schemaConfig = new SchemaConfig(columns); } diff --git a/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java b/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java index f3dc5bd..6cfbe5c 100644 --- a/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java +++ b/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java @@ -10,7 +10,6 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.embulk.EmbulkTestRuntime; import org.embulk.config.ConfigDiff; -import org.embulk.config.ConfigException; import org.embulk.config.ConfigSource; import org.embulk.config.TaskReport; import org.embulk.config.TaskSource; @@ -90,7 +89,6 @@ public void test_run_withEmptyResult() throws IOException verify(pageBuilder, times(1)).finish(); } - @Test(expected = ConfigException.class) public void test_runDynamicSchema_withEmptyResult() throws IOException { final JsonObject searchResponse = data.get("emptyResult").getAsJsonObject(); @@ -99,8 +97,8 @@ public void test_runDynamicSchema_withEmptyResult() throws IOException .thenReturn(searchResponse.get("statusCode").getAsInt()); when(response.getEntity()) .thenReturn(new StringEntity(searchResponse.get("body").toString())); - plugin.transaction(TestHelpers.dynamicSchemaConfig(), new Control()); + verify(pageBuilder, times(0)).addRecord(); } @Test From 0d382ea2946306cb232cd2fe61dc0b913dd54425 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Mon, 4 Oct 2021 10:24:15 +0700 Subject: [PATCH 20/38] Update readme and changelog --- CHANGELOG.md | 4 ++++ build.gradle | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc231ab..3d10377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.14 - 2021-10-12 +* [enhancement] Graceful exit in case no data of dynamic schema +* PR [#70](https://github.com/treasure-data/embulk-input-jira/pull/70) + ## 0.2.13 - 2021-09-30 * [enhancement] Support dynamic schema * PR [#69](https://github.com/treasure-data/embulk-input-jira/pull/69) diff --git a/build.gradle b/build.gradle index 8770524..9c0d720 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ repositories { jcenter() } -version = "0.2.13" +version = "0.2.14" group = "com.treasuredata.embulk.plugins" description = "JIRA Embulk input plugin." From e62b0d7ea067f1074284ba662d9ada5d5c0d0b2c Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Mon, 23 May 2022 15:47:10 +0700 Subject: [PATCH 21/38] Catchup with embulk 0.10.32 --- CHANGELOG.md | 4 ++++ build.gradle | 2 +- src/main/java/org/embulk/input/jira/JiraInputPlugin.java | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d10377..3590fa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.15 - 2022-05-23 +* [enhancement] Catchup with embulk v0.10.32 +* PR [#71](https://github.com/treasure-data/embulk-input-jira/pull/71) + ## 0.2.14 - 2021-10-12 * [enhancement] Graceful exit in case no data of dynamic schema * PR [#70](https://github.com/treasure-data/embulk-input-jira/pull/70) diff --git a/build.gradle b/build.gradle index 9c0d720..0934de6 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ repositories { jcenter() } -version = "0.2.14" +version = "0.2.15" group = "com.treasuredata.embulk.plugins" description = "JIRA Embulk input plugin." diff --git a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java index f7464b1..189ae47 100644 --- a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java +++ b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java @@ -172,7 +172,7 @@ public TaskReport run(final TaskSource taskSource, public ConfigDiff guess(final ConfigSource config) { // Reset columns in case already have or missing on configuration - config.set("columns", new ObjectMapper().createArrayNode()); + config.set("columns", new ArrayList<>()); final PluginTask task = CONFIG_MAPPER.map(config, PluginTask.class); JiraUtil.validateTaskConfig(task); final JiraClient jiraClient = getJiraClient(); From b5fc895d98b3a5249f9a6ce27adacdd612fdef2f Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Mon, 23 May 2022 16:16:02 +0700 Subject: [PATCH 22/38] Remove import com.fasterxml.jackson.databind.ObjectMapper --- src/main/java/org/embulk/input/jira/JiraInputPlugin.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java index 189ae47..7055c34 100644 --- a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java +++ b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java @@ -1,6 +1,5 @@ package org.embulk.input.jira; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import com.google.gson.JsonElement; import com.google.gson.JsonNull; From 2302ccb8307fc7907f12bcf909d8a24fa758be7d Mon Sep 17 00:00:00 2001 From: Dai MIKURUBE Date: Mon, 23 May 2022 18:37:51 +0900 Subject: [PATCH 23/38] Release to Maven Central in v0.3.0, under the Apache License 2.0 --- LICENSE | 202 +++++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE.txt | 21 ------ build.gradle | 110 +++++++++++++++++++++++++++- 3 files changed, 310 insertions(+), 23 deletions(-) create mode 100644 LICENSE delete mode 100644 LICENSE.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 43acdd5..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/build.gradle b/build.gradle index 0934de6..4a6663a 100644 --- a/build.gradle +++ b/build.gradle @@ -5,14 +5,14 @@ plugins { id "org.embulk.embulk-plugins" version "0.4.2" id "maven" id 'maven-publish' + id "signing" } repositories { mavenCentral() - jcenter() } -version = "0.2.15" +version = "0.3.0-SNAPSHOT" group = "com.treasuredata.embulk.plugins" description = "JIRA Embulk input plugin." @@ -21,6 +21,16 @@ targetCompatibility = 1.8 def embulkVersion = '0.10.28' +tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked" + options.encoding = "UTF-8" +} + +java { + withJavadocJar() + withSourcesJar() +} + dependencies { compileOnly "org.embulk:embulk-api:$embulkVersion" compileOnly "org.embulk:embulk-spi:$embulkVersion" @@ -99,6 +109,102 @@ embulkPlugin { type = "jira" } +javadoc { + options { + locale = "en_US" + encoding = "UTF-8" + } +} + +jar { + from rootProject.file("LICENSE") +} + +sourcesJar { + from rootProject.file("LICENSE") +} + +javadocJar { + from rootProject.file("LICENSE") +} + +// It should not publish a `.module` file in Maven Central. +// https://docs.gradle.org/current/userguide/publishing_gradle_module_metadata.html#sub:disabling-gmm-publication +tasks.withType(GenerateModuleMetadata) { + enabled = false +} + +publishing { + publications { + maven(MavenPublication) { + groupId = project.group + artifactId = project.name + + from components.java // Must be "components.java". The dependency modification works only for it. + // javadocJar and sourcesJar are added by java.withJavadocJar() and java.withSourcesJar() above. + // See: https://docs.gradle.org/current/javadoc/org/gradle/api/plugins/JavaPluginExtension.html + + pom { // https://central.sonatype.org/pages/requirements.html + packaging "jar" + + name = project.name + description = project.description + url = "https://github.com/treasure-data/embulk-input-jira" + + licenses { + license { + // http://central.sonatype.org/pages/requirements.html#license-information + name = "The Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + + developers { + // Authors after reimplementing it in Java. + developer { + name = "Phu Nguyen" + email = "phu@treasure-data.com" + } + developer { + name = "Huy Le" + email = "huy.lenq@gmail.com" + } + developer { + name = "Thanh Le" + email = "legiangthanh@gmail.com" + } + } + + scm { + connection = "scm:git:git://github.com/treasure-data/embulk-input-jira.git" + developerConnection = "scm:git:git@github.com:treasure-data/embulk-input-jira.git" + url = "https://github.com/treasure-data/embulk-input-jira" + } + } + } + } + + repositories { + maven { // publishMavenPublicationToMavenCentralRepository + name = "mavenCentral" + if (project.version.endsWith("-SNAPSHOT")) { + url "https://oss.sonatype.org/content/repositories/snapshots" + } else { + url "https://oss.sonatype.org/service/local/staging/deploy/maven2" + } + + credentials { + username = project.hasProperty("ossrhUsername") ? ossrhUsername : "" + password = project.hasProperty("ossrhPassword") ? ossrhPassword : "" + } + } + } +} + +signing { + sign publishing.publications.maven +} + jacocoTestReport { group = "Reporting" reports { From 32bddcc5086b3fe3f47c39edae27a9258dfef624 Mon Sep 17 00:00:00 2001 From: cspicer Date: Tue, 28 Feb 2023 17:19:56 -0800 Subject: [PATCH 24/38] Update CodeQL actions to v2 and enable Dependabot for GitHub Actions --- .github/dependabot.yml | 8 ++++ .github/workflows/codeql-analysis.yml | 63 --------------------------- .github/workflows/codeql.yml | 31 +++++++++++++ 3 files changed, 39 insertions(+), 63 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/codeql.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6282f6e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +--- +version: 2 +updates: + # Enable workflow version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index ae00c5f..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,63 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - - schedule: - - cron: '31 2 * * 2' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: [ 'java' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..a846189 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,31 @@ +--- +name: "CodeQL" + +on: + schedule: + - cron: '31 2 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [java] + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 From aa85cb45d16b4d56e09788056c2a96be671dff93 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Fri, 19 May 2023 08:40:53 +0700 Subject: [PATCH 25/38] Remove Travis CI configuration --- .travis.yml | 13 ------------- .travis.yml.erb | 37 ------------------------------------- 2 files changed, 50 deletions(-) delete mode 100644 .travis.yml delete mode 100644 .travis.yml.erb diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 83d575c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: java - -jdk: oraclejdk8 - -dist: trusty - -env: - global: - secure: "NCkU3f60cn5Gmzq+NwDmKdQSq+ApF/rPqSHPirk1+ZPW9n+H9912meUuJr9qyn03MPdc6wAQinsl3skdPNh1Qz02TI/WZGMiCQTLh3nKIMzaaxx6I+OZ1TV7A4MoJMQ7oYQdXdbvRYXpOVRnfVDxpUC+BDk2T8xmjfm9cht+PGTzM6RxpmSL842hiudkcyxHxgEf66EGpi6h2G2PqYNfQrksqiVMWlLiTO5a2ee+iw9qTMhgKrhEKLCb+dFB56gtKIOpd3MxieLwf3PDcsjM+TSaWo6Bp+mjdA3zGJNz5wbWRVSdrIvKvxmf3eRqAGssBhBDO/LUN3LnI+/T/7J9XZZd7WXNudo/t6HQXhJkUIvOucCxSg8/uRsxrMAmF2zkJOnYGSvN3HNqdzyz86TK4/Xckl+gX51b0uYncHfZYSojn4py1RWPYpX/L2RHbzMn1MJ7B3/VZ21vY97aHhDgNyyAMxPnTAuR1IZicEgA8y8AmZkkK9tWq5wvAOKI91V+W8iBEaO2dZIib+KdJSUUj/Q3TM3i4wX9O/AU7tknsnCApvxQeveELJ4SxfFd/Z2imqmQWM0wIgp1u7Vjguf8kSaxcuQHonZNdwvvYOkbYxHvUqltzlmE1qFC/kQe6o9fS3NuiugHCYVeFs+LXVVW+nl0pWztFYMicUmQq7T781w=" - -script: - - ./gradlew gem - - ./gradlew checkstyle test jacocoTestReport diff --git a/.travis.yml.erb b/.travis.yml.erb deleted file mode 100644 index f9de8bc..0000000 --- a/.travis.yml.erb +++ /dev/null @@ -1,37 +0,0 @@ -language: ruby - -jdk: oraclejdk8 - -env: - global: - secure: "NCkU3f60cn5Gmzq+NwDmKdQSq+ApF/rPqSHPirk1+ZPW9n+H9912meUuJr9qyn03MPdc6wAQinsl3skdPNh1Qz02TI/WZGMiCQTLh3nKIMzaaxx6I+OZ1TV7A4MoJMQ7oYQdXdbvRYXpOVRnfVDxpUC+BDk2T8xmjfm9cht+PGTzM6RxpmSL842hiudkcyxHxgEf66EGpi6h2G2PqYNfQrksqiVMWlLiTO5a2ee+iw9qTMhgKrhEKLCb+dFB56gtKIOpd3MxieLwf3PDcsjM+TSaWo6Bp+mjdA3zGJNz5wbWRVSdrIvKvxmf3eRqAGssBhBDO/LUN3LnI+/T/7J9XZZd7WXNudo/t6HQXhJkUIvOucCxSg8/uRsxrMAmF2zkJOnYGSvN3HNqdzyz86TK4/Xckl+gX51b0uYncHfZYSojn4py1RWPYpX/L2RHbzMn1MJ7B3/VZ21vY97aHhDgNyyAMxPnTAuR1IZicEgA8y8AmZkkK9tWq5wvAOKI91V+W8iBEaO2dZIib+KdJSUUj/Q3TM3i4wX9O/AU7tknsnCApvxQeveELJ4SxfFd/Z2imqmQWM0wIgp1u7Vjguf8kSaxcuQHonZNdwvvYOkbYxHvUqltzlmE1qFC/kQe6o9fS3NuiugHCYVeFs+LXVVW+nl0pWztFYMicUmQq7T781w=" - -before_install: - - | - ruby -v - # Currently, Travis can't treat jruby 9.0.5.0 - rvm get head - rvm use jruby-9.0.5.0 --install - ruby -v - - gem i bundler - -rvm: jruby-9.0.5.0 - -gemfile: -<% versions.each do |file| -%> - - gemfiles/<%= file %> -<% end -%> - -matrix: - exclude: - - jdk: oraclejdk8 # Ignore all matrix at first, use `include` to allow build - include: - <% matrix.each do |m| -%> -<%= m %> - <% end %> - - allow_failures: - # Ignore failure for *-latest - <% versions.find_all{|file| file.to_s.match(/-latest/)}.each do |file| -%> -- gemfile: <%= file %> - <% end %> From 0c5f437260c47e23b50f1a4ad385c79f2c084427 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 May 2023 02:10:32 +0000 Subject: [PATCH 26/38] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a846189..0cd0dbc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Initialize CodeQL uses: github/codeql-action/init@v2 From 6724475d54788600cbeb0577b9c4d9d6dbe108e2 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Fri, 19 May 2023 09:26:00 +0700 Subject: [PATCH 27/38] Update embulk version, minor refactor code --- build.gradle | 11 +++++---- .../embulkPluginRuntime.lockfile | 2 +- .../java/org/embulk/input/jira/Issue.java | 4 +--- .../embulk/input/jira/client/JiraClient.java | 4 +--- .../org/embulk/input/jira/util/JiraUtil.java | 24 +++++++------------ 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/build.gradle b/build.gradle index 4a6663a..8393a46 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ description = "JIRA Embulk input plugin." sourceCompatibility = 1.8 targetCompatibility = 1.8 -def embulkVersion = '0.10.28' +def embulkVersion = '0.10.32' tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked" @@ -46,12 +46,12 @@ dependencies { compile 'org.embulk:embulk-util-timestamp:0.2.1' compile('org.embulk:embulk-util-guess:0.1.2') - compile('org.embulk:embulk-util-json:0.1.0') { + compile('org.embulk:embulk-util-json:0.1.1') { exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' exclude group: 'org.msgpack', module: 'msgpack-core' } - // Explicit dependencies for embulk-util-* that matches with Embulk 0.10.28 + // Explicit dependencies for embulk-util-* that matches with Embulk 0.10.32 compile 'com.fasterxml.jackson.core:jackson-core:2.6.7' compile 'com.fasterxml.jackson.core:jackson-annotations:2.6.7' compile 'com.fasterxml.jackson.core:jackson-databind:2.6.7' @@ -79,10 +79,11 @@ dependencies { exclude group: 'com.fasterxml.jackson.datatype', module: 'jackson-datatype-jdk8' exclude group: 'javax.validation', module: 'validation-api' } - testCompile "org.embulk:embulk-standards:$embulkVersion" + testCompile "junit:junit:4.10" + testCompile "org.embulk:embulk-junit4:$embulkVersion" + testCompile "org.embulk:embulk-core:$embulkVersion" testCompile "org.embulk:embulk-core:$embulkVersion:tests" testCompile "org.embulk:embulk-deps:$embulkVersion" - testCompile "org.embulk:embulk-junit4:$embulkVersion" testCompile "org.mockito:mockito-core:2.28.2" } diff --git a/gradle/dependency-locks/embulkPluginRuntime.lockfile b/gradle/dependency-locks/embulkPluginRuntime.lockfile index 086251a..b9b65dd 100644 --- a/gradle/dependency-locks/embulkPluginRuntime.lockfile +++ b/gradle/dependency-locks/embulkPluginRuntime.lockfile @@ -27,7 +27,7 @@ org.eclipse.jetty:jetty-util:9.2.14.v20151106 org.embulk:embulk-util-config:0.3.1 org.embulk:embulk-util-file:0.1.1 org.embulk:embulk-util-guess:0.1.2 -org.embulk:embulk-util-json:0.1.0 +org.embulk:embulk-util-json:0.1.1 org.embulk:embulk-util-retryhelper-jetty92:0.8.2 org.embulk:embulk-util-retryhelper:0.8.2 org.embulk:embulk-util-rubytime:0.3.2 diff --git a/src/main/java/org/embulk/input/jira/Issue.java b/src/main/java/org/embulk/input/jira/Issue.java index 01b48a2..e809c22 100644 --- a/src/main/java/org/embulk/input/jira/Issue.java +++ b/src/main/java/org/embulk/input/jira/Issue.java @@ -53,9 +53,7 @@ else if (keys.isEmpty() || (json.isJsonArray() && json.getAsJsonArray().size() = } return get(arrays, keys); } - else { - return get(json.getAsJsonObject().get(key), keys); - } + return get(json.getAsJsonObject().get(key), keys); } public synchronized JsonObject getFlatten() diff --git a/src/main/java/org/embulk/input/jira/client/JiraClient.java b/src/main/java/org/embulk/input/jira/client/JiraClient.java index ffd3cd9..b5bd8b6 100644 --- a/src/main/java/org/embulk/input/jira/client/JiraClient.java +++ b/src/main/java/org/embulk/input/jira/client/JiraClient.java @@ -60,9 +60,7 @@ public void checkUserCredentials(final PluginTask task) if (e.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { throw new ConfigException("Could not authorize with your credential."); } - else { - throw new ConfigException("Could not authorize with your credential due to problems when contacting JIRA API."); - } + throw new ConfigException("Could not authorize with your credential due to problems when contacting JIRA API."); } } diff --git a/src/main/java/org/embulk/input/jira/util/JiraUtil.java b/src/main/java/org/embulk/input/jira/util/JiraUtil.java index d25a1af..e4c2b67 100644 --- a/src/main/java/org/embulk/input/jira/util/JiraUtil.java +++ b/src/main/java/org/embulk/input/jira/util/JiraUtil.java @@ -116,13 +116,12 @@ private static Instant getTimestampValue(final PluginTask task, final Column col .builder(pattern, true) .setDefaultZoneFromString("UTC") .build(); - Instant result = null; try { - result = formatter.parse(value); + return formatter.parse(value); } catch (final Exception e) { + return null; } - return result; } /* @@ -131,13 +130,12 @@ private static Instant getTimestampValue(final PluginTask task, final Column col * */ private static Long getLongValue(final JsonElement value) { - Long result = null; try { - result = value.getAsLong(); + return value.getAsLong(); } catch (final Exception e) { + return null; } - return result; } /* @@ -146,13 +144,12 @@ private static Long getLongValue(final JsonElement value) * */ private static Double getDoubleValue(final JsonElement value) { - Double result = null; try { - result = value.getAsDouble(); + return value.getAsDouble(); } catch (final Exception e) { + return null; } - return result; } /* @@ -161,13 +158,12 @@ private static Double getDoubleValue(final JsonElement value) * */ private static Boolean getBooleanValue(final JsonElement value) { - Boolean result = null; try { - result = value.getAsBoolean(); + return value.getAsBoolean(); } catch (final Exception e) { + return null; } - return result; } public static void addRecord(final Issue issue, final Schema schema, final PluginTask task, final PageBuilder pageBuilder) @@ -201,9 +197,7 @@ else if (data.isJsonArray()) { if (obj.isJsonPrimitive()) { return obj.getAsString(); } - else { - return obj.toString(); - } + return obj.toString(); }) .collect(Collectors.joining(","))); } From 57e43e644386253b1fa5112ec5f881374b566e9c Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Fri, 19 May 2023 09:33:46 +0700 Subject: [PATCH 28/38] Update readme and version --- README.md | 10 +++++++--- build.gradle | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5307680..8a6defe 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,15 @@ in: ## Build ``` -$ ./gradlew gem # -t to watch change of files and rebuild continuously +$ ./gradlew checkstyle test jacocoTestReport; ./gradlew gem; ``` -## Test +## Build and Test With Local File +``` +$ rm -rf build; ./gradlew gem; embulk guess -L ./build/gemContents/ {path_to_yaml_file} +``` +## Publish ``` -$ ./gradlew checkstyle test jacocoTestReport +$ rm -rf build; ./gradlew gem; ./gradlew publishMavenPublicationToMavenCentralRepository ``` \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8393a46..7e2130f 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ repositories { mavenCentral() } -version = "0.3.0-SNAPSHOT" +version = "0.3.1-SNAPSHOT" group = "com.treasuredata.embulk.plugins" description = "JIRA Embulk input plugin." From 27f0f70b1108992aca454283c91912cc67f86ba6 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Fri, 19 May 2023 09:48:32 +0700 Subject: [PATCH 29/38] Add github action to check commit and PR --- .github/workflows/check.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/check.yml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..2a2e03b --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,20 @@ +name: Check + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up OpenJDK 8 + uses: actions/setup-java@v3 + with: + java-version: 8 + distribution: "zulu" + - name: Check + run: ./gradlew checkstyle test jacocoTestReport \ No newline at end of file From 2b4f78c2cdb8b190c640a3818a5eeb0bbb79bf80 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Fri, 19 May 2023 14:01:34 +0700 Subject: [PATCH 30/38] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3590fa5..d1fdf87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.1-SNAPSHOT - 2023-05-19 +* [enhancement] Update library, minor code refactoring +* PR [#78](https://github.com/treasure-data/embulk-input-jira/pull/78) + ## 0.2.15 - 2022-05-23 * [enhancement] Catchup with embulk v0.10.32 * PR [#71](https://github.com/treasure-data/embulk-input-jira/pull/71) From 3d91943a9b0f6afb5a04c2bc1f54632798ca7479 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Fri, 19 May 2023 16:51:29 +0700 Subject: [PATCH 31/38] Fix gradle test dependencies --- build.gradle | 8 -------- 1 file changed, 8 deletions(-) diff --git a/build.gradle b/build.gradle index 7e2130f..a9cb0b4 100644 --- a/build.gradle +++ b/build.gradle @@ -71,14 +71,6 @@ dependencies { compile 'com.google.guava:guava:18.0' compile 'org.apache.commons:commons-lang3:3.4' - testCompile "junit:junit:4.13" - testCompile("org.embulk:embulk-core:$embulkVersion") { - exclude group: 'com.fasterxml.jackson.core', module: 'jackson-annotations' - exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' - exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind' - exclude group: 'com.fasterxml.jackson.datatype', module: 'jackson-datatype-jdk8' - exclude group: 'javax.validation', module: 'validation-api' - } testCompile "junit:junit:4.10" testCompile "org.embulk:embulk-junit4:$embulkVersion" testCompile "org.embulk:embulk-core:$embulkVersion" From 9f4b1852141a35ed73190b625e0690c30be9783c Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Fri, 19 May 2023 16:52:40 +0700 Subject: [PATCH 32/38] Unify artifacts --- .github/workflows/check.yml | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 2a2e03b..fba4b71 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -10,11 +10,32 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up OpenJDK 8 - uses: actions/setup-java@v3 - with: - java-version: 8 - distribution: "zulu" - - name: Check - run: ./gradlew checkstyle test jacocoTestReport \ No newline at end of file + - uses: actions/checkout@v3 + + - name: Set up JDK 1.8 + uses: actions/setup-java@v3 + with: + java-version: 8 + distribution: "zulu" + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with testing + run: ./gradlew check --console rich --info + + - uses: actions/upload-artifact@v3 + if: always() + with: + name: tests + path: ./build/reports/tests/test + + - name: JaCoCo test report + if: success() + run: ./gradlew jacocoTestReport + + - uses: actions/upload-artifact@v3 + if: success() + with: + name: jacoco + path: ./build/reports/coverage From 637fb0eafc23095ab00c89ddba259e5f41d5e27e Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Mon, 22 May 2023 09:41:24 +0700 Subject: [PATCH 33/38] Update plugin version --- CHANGELOG.md | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1fdf87..cfc819f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.3.1-SNAPSHOT - 2023-05-19 +## 0.3.1 - 2023-05-19 * [enhancement] Update library, minor code refactoring * PR [#78](https://github.com/treasure-data/embulk-input-jira/pull/78) diff --git a/build.gradle b/build.gradle index a9cb0b4..70213e3 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ repositories { mavenCentral() } -version = "0.3.1-SNAPSHOT" +version = "0.3.1" group = "com.treasuredata.embulk.plugins" description = "JIRA Embulk input plugin." From bdf25f30ba4ad8062375261bcb33e732362b21d1 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Fri, 8 Nov 2024 09:20:53 +0700 Subject: [PATCH 34/38] Update upload artifact to v4 --- .github/workflows/check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index fba4b71..abada03 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -24,7 +24,7 @@ jobs: - name: Build with testing run: ./gradlew check --console rich --info - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: always() with: name: tests @@ -34,7 +34,7 @@ jobs: if: success() run: ./gradlew jacocoTestReport - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: success() with: name: jacoco From 33b6f4d3fca51a07ee0ec47919486d46462eda15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 03:42:46 +0000 Subject: [PATCH 35/38] Bump actions/setup-java from 3 to 4 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 3 to 4. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index abada03..ada4c8f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up JDK 1.8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 8 distribution: "zulu" From 77f74cbb0d01fe6ccf8f42680ecd08850121f172 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 03:45:15 +0000 Subject: [PATCH 36/38] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/check.yml | 2 +- .github/workflows/codeql.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ada4c8f..2309ca6 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 1.8 uses: actions/setup-java@v4 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0cd0dbc..b7eb13b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v2 From 78ce6a76798242f5381dcdccf20383fe5cc7833f Mon Sep 17 00:00:00 2001 From: Tetsuro Sano Date: Fri, 6 Dec 2024 23:35:04 +0900 Subject: [PATCH 37/38] Support for Embulk v0.9 --- src/main/java/org/embulk/input/jira/JiraInputPlugin.java | 3 ++- src/main/java/org/embulk/input/jira/util/JiraUtil.java | 4 +++- src/test/java/org/embulk/input/jira/util/JiraUtilTest.java | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java index 646a036..1140836 100644 --- a/src/main/java/org/embulk/input/jira/JiraInputPlugin.java +++ b/src/main/java/org/embulk/input/jira/JiraInputPlugin.java @@ -233,9 +233,10 @@ private List> createGuessSample(final List } @VisibleForTesting + @SuppressWarnings("deprecation") // TODO: For compatibility with Embulk v0.9 public PageBuilder getPageBuilder(final Schema schema, final PageOutput output) { - return Exec.getPageBuilder(Exec.getBufferAllocator(), schema, output); + return new PageBuilder(Exec.getBufferAllocator(), schema, output); // TODO: Use Exec#getPageBuilder } @VisibleForTesting diff --git a/src/main/java/org/embulk/input/jira/util/JiraUtil.java b/src/main/java/org/embulk/input/jira/util/JiraUtil.java index e4c2b67..f3ab47a 100644 --- a/src/main/java/org/embulk/input/jira/util/JiraUtil.java +++ b/src/main/java/org/embulk/input/jira/util/JiraUtil.java @@ -207,6 +207,7 @@ else if (data.isJsonArray()) { } @Override + @SuppressWarnings("deprecation") // TODO: For compatibility with Embulk v0.9 public void timestampColumn(final Column column) { final JsonElement data = issue.getValue(column.getName()); @@ -219,7 +220,8 @@ public void timestampColumn(final Column column) pageBuilder.setNull(column); } else { - pageBuilder.setTimestamp(column, value); + // TODO: Use Instant instead of Timestamp + pageBuilder.setTimestamp(column, org.embulk.spi.time.Timestamp.ofInstant(value)); } } } diff --git a/src/test/java/org/embulk/input/jira/util/JiraUtilTest.java b/src/test/java/org/embulk/input/jira/util/JiraUtilTest.java index 460aa80..85d5c52 100644 --- a/src/test/java/org/embulk/input/jira/util/JiraUtilTest.java +++ b/src/test/java/org/embulk/input/jira/util/JiraUtilTest.java @@ -256,6 +256,7 @@ public void test_validateTaskConfig_RetryLimitLessThan0() throws IOException } @Test + @SuppressWarnings("deprecation") // TODO: For compatibility with Embulk v0.9 public void test_addRecord_allRight() { String testName = "allRight"; @@ -278,7 +279,8 @@ public void test_addRecord_allRight() verify(mock, times(1)).setLong(longColumn, longValue); verify(mock, times(1)).setDouble(doubleColumn, doubleValue); verify(mock, times(1)).setString(stringColumn, stringValue); - verify(mock, times(1)).setTimestamp(dateColumn, dateValue); + // TODO: Use Instant instead of Timestamp + verify(mock, times(1)).setTimestamp(dateColumn, org.embulk.spi.time.Timestamp.ofInstant(dateValue)); verify(mock, times(1)).setJson(jsonColumn, jsonValue); } From 723a31adafa432e924c6255a522cbdc03c4c402f Mon Sep 17 00:00:00 2001 From: Tetsuro Sano Date: Mon, 9 Dec 2024 22:32:29 +0900 Subject: [PATCH 38/38] Fix workflows --- .github/CODEOWNERS | 1 - .github/workflows/gem-push.yml | 27 +++++++++++++++++++++++++++ build.gradle | 25 +++++++++++++++++-------- 3 files changed, 44 insertions(+), 9 deletions(-) delete mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/gem-push.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 7876f5a..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @treasure-data/integrations diff --git a/.github/workflows/gem-push.yml b/.github/workflows/gem-push.yml new file mode 100644 index 0000000..d51f9e8 --- /dev/null +++ b/.github/workflows/gem-push.yml @@ -0,0 +1,27 @@ +name: Ruby Gem + +on: + workflow_dispatch: + push: + tags: + - '*' + +jobs: + build: + name: Build + Publish + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby 2.7 + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + - name: push gem + uses: trocco-io/push-gem-to-gpr-action@v2 + with: + language: java + gem-path: "./build/gems/*.gem" + github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/build.gradle b/build.gradle index 70213e3..e33afda 100644 --- a/build.gradle +++ b/build.gradle @@ -3,17 +3,25 @@ plugins { id "checkstyle" id "jacoco" id "org.embulk.embulk-plugins" version "0.4.2" - id "maven" - id 'maven-publish' - id "signing" + id "com.palantir.git-version" version "3.0.0" +// id "maven" +// id 'maven-publish' +// id "signing" } repositories { mavenCentral() } -version = "0.3.1" -group = "com.treasuredata.embulk.plugins" +group = "trocco-io" +version = { + def vd = versionDetails() + if (vd.commitDistance == 0 && vd.lastTag ==~ /^[0-9]+\.[0-9]+\.[0-9]+([.-][.a-zA-Z0-9-]+)?/) { + vd.lastTag + } else { + "0.0.0.${vd.gitHash}" + } +}() description = "JIRA Embulk input plugin." sourceCompatibility = 1.8 @@ -126,7 +134,7 @@ javadocJar { tasks.withType(GenerateModuleMetadata) { enabled = false } - +/* publishing { publications { maven(MavenPublication) { @@ -197,7 +205,7 @@ publishing { signing { sign publishing.publications.maven } - +*/ jacocoTestReport { group = "Reporting" reports { @@ -214,7 +222,8 @@ gem { homepage = 'https://github.com/treasure-data/embulk-input-jira' licenses = ['Apache-2.0'] } - +/* gemPush { host = "https://rubygems.org" } +*/