├── yarnLint.sh ├── yarnTest.sh ├── yarnInstall.sh ├── yarnWatchGradle.sh ├── .circleci ├── images │ └── primary │ │ ├── cacerts │ │ └── Dockerfile └── config.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── resources │ │ ├── static │ │ │ ├── javascripts │ │ │ │ ├── store │ │ │ │ │ ├── model │ │ │ │ │ │ ├── Request.ts │ │ │ │ │ │ ├── ResponseBase.ts │ │ │ │ │ │ ├── ResponseTransferObject.ts │ │ │ │ │ │ ├── ResponseTable.ts │ │ │ │ │ │ ├── ResponseText.ts │ │ │ │ │ │ └── ResponseTableRow.ts │ │ │ │ │ ├── error │ │ │ │ │ │ └── UserCancelError.ts │ │ │ │ │ ├── action-types.ts │ │ │ │ │ ├── State.ts │ │ │ │ │ ├── getters.ts │ │ │ │ │ ├── mutation-types.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── actions.ts │ │ │ │ │ └── mutations.ts │ │ │ │ ├── vue-shims.d.ts │ │ │ │ ├── index.ts │ │ │ │ ├── App.vue │ │ │ │ ├── App.ts │ │ │ │ ├── test │ │ │ │ │ ├── tsconfig.json │ │ │ │ │ └── store │ │ │ │ │ │ └── mutations.spec.ts │ │ │ │ ├── components │ │ │ │ │ ├── ResultText.ts │ │ │ │ │ ├── ResultText.vue │ │ │ │ │ ├── Result.ts │ │ │ │ │ ├── Result.vue │ │ │ │ │ ├── Editor.ts │ │ │ │ │ ├── ResultTable.ts │ │ │ │ │ ├── Editor.vue │ │ │ │ │ └── ResultTable.vue │ │ │ │ └── api │ │ │ │ │ └── index.ts │ │ │ ├── tslint.json │ │ │ ├── tsconfig.json │ │ │ ├── package.json │ │ │ └── webpack.config.js │ │ ├── application.yml │ │ └── templates │ │ │ └── index.html │ └── kotlin │ │ └── info │ │ └── matsumana │ │ └── tsujun │ │ ├── model │ │ ├── ksql │ │ │ ├── KsqlRequest.kt │ │ │ ├── KsqlResponseErrorMessage.kt │ │ │ ├── KsqlResponseStreams.kt │ │ │ ├── KsqlResponseTables.kt │ │ │ ├── KsqlResponseQueries.kt │ │ │ └── KsqlResponseSelect.kt │ │ ├── Request.kt │ │ ├── ResponseError.kt │ │ └── ResponseTable.kt │ │ ├── exception │ │ └── KsqlException.kt │ │ ├── TsujunApplication.kt │ │ ├── controller │ │ ├── RootController.kt │ │ ├── SqlController.kt │ │ └── ControllerAdvisor.kt │ │ ├── config │ │ └── KsqlServerConfig.kt │ │ └── service │ │ └── KsqlService.kt └── test │ └── kotlin │ └── info │ └── matsumana │ └── tsujun │ └── TsujunApplicationTest.kt ├── yarnWatchIntelliJ.sh ├── yarnBuildProd.sh ├── .gitignore ├── Dockerfile ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /yarnLint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd src/main/resources/static/javascripts 4 | 5 | yarn lint 6 | -------------------------------------------------------------------------------- /yarnTest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd src/main/resources/static/javascripts 4 | 5 | yarn test 6 | -------------------------------------------------------------------------------- /yarnInstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd src/main/resources/static/javascripts 4 | 5 | yarn install 6 | -------------------------------------------------------------------------------- /yarnWatchGradle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd src/main/resources/static/javascripts 4 | 5 | yarn watch 6 | -------------------------------------------------------------------------------- /.circleci/images/primary/cacerts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/tsujun/master/.circleci/images/primary/cacerts -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/tsujun/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/model/Request.ts: -------------------------------------------------------------------------------- 1 | export class Request { 2 | sequence: number; 3 | sql: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/vue-shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/model/ksql/KsqlRequest.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.model.ksql 2 | 3 | data class KsqlRequest(val ksql: String) 4 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/model/ResponseBase.ts: -------------------------------------------------------------------------------- 1 | export interface ResponseBase { 2 | sequence: number; 3 | sql: string; 4 | mode: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/model/Request.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.model 2 | 3 | data class Request(val sequence: Int, 4 | val sql: String) 5 | -------------------------------------------------------------------------------- /yarnWatchIntelliJ.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd src/main/resources/static/javascripts 4 | 5 | export JS_OUTPUT_PATH=../../../../out/production/resources/static/javascripts 6 | 7 | yarn watch 8 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/model/ResponseTransferObject.ts: -------------------------------------------------------------------------------- 1 | export interface ResponseTransferObject { 2 | sequence: number; 3 | payload: string; 4 | errorMessage: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/model/ResponseTable.ts: -------------------------------------------------------------------------------- 1 | import { ResponseBase } from './ResponseBase'; 2 | 3 | export interface ResponseTable extends ResponseBase { 4 | data: any[][]; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/model/ResponseText.ts: -------------------------------------------------------------------------------- 1 | import { ResponseBase } from './ResponseBase'; 2 | 3 | export interface ResponseText extends ResponseBase { 4 | text: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/model/ResponseTableRow.ts: -------------------------------------------------------------------------------- 1 | import { ResponseBase } from './ResponseBase'; 2 | 3 | export interface ResponseTableRow extends ResponseBase { 4 | data: any[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import store from './store'; 4 | 5 | new Vue({ 6 | store, 7 | render: h => h(App), 8 | }).$mount('#app'); 9 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/error/UserCancelError.ts: -------------------------------------------------------------------------------- 1 | export class UserCancelError implements Error { 2 | 3 | name = 'UserCancelError'; 4 | 5 | constructor(public message: string) { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/action-types.ts: -------------------------------------------------------------------------------- 1 | const INPUT_SQL = 'INPUT_SQL'; 2 | const SUBMIT = 'SUBMIT'; 3 | const CANCEL = 'CANCEL'; 4 | 5 | export const ACTION = { 6 | INPUT_SQL, 7 | SUBMIT, 8 | CANCEL, 9 | }; 10 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/model/ResponseError.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.model 2 | 3 | data class ResponseError(val sequence: Int, 4 | val sql: String, 5 | val message: String) 6 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/model/ksql/KsqlResponseErrorMessage.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.model.ksql 2 | 3 | data class KsqlResponseErrorMessage(val message: String, 4 | val stackTrace: List) 5 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | management: 2 | endpoints: 3 | web: 4 | exposure: 5 | include: health,env,loggers,httptrace,heapdump,threaddump,prometheus 6 | 7 | logging: 8 | level: 9 | info.matsumana.tsujun: INFO 10 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/State.ts: -------------------------------------------------------------------------------- 1 | import { ResponseBase } from './model/ResponseBase'; 2 | 3 | export class State { 4 | sequence = 0; 5 | sql = ''; 6 | results: ResponseBase[] = []; 7 | cancels: Set = new Set(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/getters.ts: -------------------------------------------------------------------------------- 1 | import { GetterTree } from 'vuex'; 2 | import { State } from './State'; 3 | 4 | const getters = > { 5 | results: (state: State) => state.results, 6 | }; 7 | 8 | export default getters; 9 | -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tsūjun 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Nov 28 23:17:09 JST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-all.zip 7 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/exception/KsqlException.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.exception 2 | 3 | class KsqlException(val sequence: Int, 4 | val sql: String, 5 | val statusCode: Int, 6 | override val message: String) : RuntimeException() 7 | -------------------------------------------------------------------------------- /src/main/resources/static/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "tslint-config-airbnb" ], 3 | "rules": { 4 | "max-line-length": [ true, 140 ], 5 | "object-shorthand-properties-first": false, 6 | "radix": false, 7 | "ter-arrow-parens": false, 8 | "import-name": false, 9 | "function-name": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/mutation-types.ts: -------------------------------------------------------------------------------- 1 | const INPUT_SQL = 'INPUT_SQL'; 2 | const SUBMIT = 'SUBMIT'; 3 | const SUBMITED = 'SUBMITED'; 4 | const ON_RESPONSE = 'ON_RESPONSE'; 5 | const CANCEL = 'CANCEL'; 6 | 7 | export const MUTATION = { 8 | INPUT_SQL, 9 | SUBMIT, 10 | SUBMITED, 11 | ON_RESPONSE, 12 | CANCEL, 13 | }; 14 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/App.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Component from 'vue-class-component'; 3 | import Editor from './components/Editor.vue'; 4 | import Result from './components/Result.vue'; 5 | 6 | @Component({ 7 | components: { 8 | Editor, 9 | Result, 10 | }, 11 | }) 12 | export default class App extends Vue { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "commonjs", 5 | "target": "ES5", 6 | "strict": true, 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "noImplicitThis": true, 10 | "experimentalDecorators": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/TsujunApplication.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | class TsujunApplication 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) 11 | } 12 | -------------------------------------------------------------------------------- /yarnBuildProd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH 4 | 5 | cd src/main/resources/static/javascripts 6 | 7 | yarn build:prod 8 | 9 | cd ../../../../../build/resources/main/static 10 | 11 | rm -rf node_modules 12 | rm -rf *.* 13 | 14 | mv ./javascripts/bundle.js . 15 | rm -rf ./javascripts/* 16 | mv ./bundle.js ./javascripts 17 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import { State } from './State'; 4 | import actions from './actions'; 5 | import mutations from './mutations'; 6 | import getters from './getters'; 7 | 8 | Vue.use(Vuex); 9 | 10 | export default new Vuex.Store({ 11 | state: new State(), 12 | actions, 13 | mutations, 14 | getters, 15 | }); 16 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/model/ksql/KsqlResponseStreams.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.model.ksql 2 | 3 | data class KsqlResponseStreams(val streams: KsqlResponseStreamsInner) 4 | 5 | data class KsqlResponseStreamsInner(val statementText: String, val streams: Array) 6 | 7 | data class KsqlResponseStreamsInnerStreams(val name: String, val topic: String, val format: String) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/model/ksql/KsqlResponseTables.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.model.ksql 2 | 3 | data class KsqlResponseTables(val tables: KsqlResponseTablesInner) 4 | 5 | data class KsqlResponseTablesInner(val statementText: String, val tables: Array) 6 | 7 | data class KsqlResponseTablesInnerTables(val name: String, val topic: String, val format: String, val isWindowed: Boolean) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | /out/ 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | nbproject/private/ 22 | build/ 23 | nbbuild/ 24 | dist/ 25 | nbdist/ 26 | .nb-gradle/ 27 | 28 | ### npm ### 29 | node_modules/ 30 | -------------------------------------------------------------------------------- /src/main/resources/static/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "ES2015", 5 | "target": "ES5", 6 | "lib": [ "dom", "es6", "es2015.collection" ], 7 | "sourceMap": true, 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "strictNullChecks": false, 12 | "noImplicitThis": true, 13 | "experimentalDecorators": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/components/ResultText.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Component from 'vue-class-component'; 3 | import { Prop } from 'vue-property-decorator'; 4 | 5 | @Component 6 | export default class ResultText extends Vue { 7 | @Prop() 8 | readonly sequence: number; 9 | 10 | @Prop() 11 | readonly sql: string; 12 | 13 | @Prop() 14 | readonly mode: number; 15 | 16 | @Prop() 17 | readonly text: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/controller/RootController.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.controller 2 | 3 | import org.springframework.stereotype.Controller 4 | import org.springframework.web.bind.annotation.GetMapping 5 | import org.springframework.web.bind.annotation.RequestMapping 6 | 7 | @Controller 8 | @RequestMapping("/") 9 | class RootController { 10 | 11 | @GetMapping 12 | fun index(): String { 13 | return "index" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/components/ResultText.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/config/KsqlServerConfig.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.config 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | import org.springframework.context.annotation.Configuration 5 | 6 | /** 7 | * see also: 8 | * https://github.com/spring-projects/spring-boot/wiki/Relaxed-Binding-2.0#environment-variables 9 | */ 10 | @Configuration 11 | @ConfigurationProperties(prefix = "ksql") 12 | data class KsqlServerConfig(var server: String = "http://localhost:8080") 13 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/model/ksql/KsqlResponseQueries.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.model.ksql 2 | 3 | data class KsqlResponseQueries(val queries: KsqlResponseQueriesInner) 4 | 5 | data class KsqlResponseQueriesInner(val statementText: String, val queries: Array) 6 | 7 | data class KsqlResponseQueriesInnerQueries(val id: KsqlResponseQueriesInnerQueriesId, val kafkaTopic: String, val queryString: String) 8 | 9 | data class KsqlResponseQueriesInnerQueriesId(val id: String) 10 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/components/Result.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Component from 'vue-class-component'; 3 | import store from '../store'; 4 | import ResultText from './ResultText.vue'; 5 | import ResultTable from './ResultTable.vue'; 6 | 7 | @Component({ 8 | components: { 9 | ResultText, 10 | ResultTable, 11 | }, 12 | }) 13 | export default class Result extends Vue { 14 | 15 | // --- computed -------------------------------------------- 16 | get results(): string { 17 | return store.getters.results; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.circleci/images/primary/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:9.0.1 2 | 3 | # cacerts from JDK 8u152 to workaround http://bugs.java.com/view_bug.do?bug_id=8189357 4 | # 5 | # see also: 6 | # https://github.com/docker-library/openjdk/issues/145 7 | # https://github.com/keeganwitt/docker-gradle/blob/1d0a9b199274b66cbb247279bb50ceaacdfb2e31/jdk9/Dockerfile 8 | COPY cacerts /etc/ssl/certs/java/cacerts 9 | 10 | # install Node.js 11 | RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \ 12 | apt-get install -y nodejs 13 | 14 | # install yarn 15 | RUN npm install -g yarn 16 | -------------------------------------------------------------------------------- /src/test/kotlin/info/matsumana/tsujun/TsujunApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun 2 | 3 | import info.matsumana.tsujun.service.KsqlService 4 | import org.junit.jupiter.api.Assertions 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig 8 | 9 | @SpringJUnitConfig(TsujunApplication::class) 10 | class TsujunApplicationTest { 11 | 12 | @Autowired lateinit var ksqlService: KsqlService 13 | 14 | @Test 15 | fun contextLoads() { 16 | Assertions.assertNotNull(ksqlService) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # for build 2 | FROM openjdk:8 AS build-env 3 | 4 | # install Node.js 5 | RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \ 6 | apt-get install -y nodejs 7 | 8 | # install yarn 9 | RUN npm install -g yarn 10 | 11 | # compile app 12 | RUN mkdir /root/tsujun 13 | COPY . /root/tsujun 14 | WORKDIR /root/tsujun 15 | RUN rm -rf src/main/resources/static/node_modules 16 | RUN ./yarnInstall.sh 17 | RUN ./gradlew clean build 18 | 19 | 20 | 21 | # for runtime 22 | FROM openjdk:8 23 | 24 | COPY --from=build-env /root/tsujun/build/libs/tsujun-*.jar /root/tsujun.jar 25 | 26 | EXPOSE 8080 27 | 28 | CMD ["java", "-jar", "/root/tsujun.jar"] 29 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/components/Result.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/model/ksql/KsqlResponseSelect.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.model.ksql 2 | 3 | import java.util.* 4 | 5 | data class KsqlResponseSelect(val row: KsqlResponseSelectColumns) 6 | 7 | data class KsqlResponseSelectColumns(val columns: Array) { 8 | 9 | override fun equals(other: Any?): Boolean { 10 | if (this === other) return true 11 | if (javaClass != other?.javaClass) return false 12 | 13 | other as KsqlResponseSelectColumns 14 | 15 | if (!Arrays.equals(columns, other.columns)) return false 16 | 17 | return true 18 | } 19 | 20 | override fun hashCode(): Int { 21 | return Arrays.hashCode(columns) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/components/Editor.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Component from 'vue-class-component'; 3 | import Brace from 'vue-bulma-brace/src/Brace.vue'; 4 | import { ACTION } from '../store/action-types'; 5 | import store from '../store'; 6 | 7 | @Component({ 8 | components: { 9 | Brace, 10 | }, 11 | }) 12 | export default class Editor extends Vue { 13 | 14 | // --- input field ----------------------------------------- 15 | sql = ''; 16 | 17 | // --- method ---------------------------------------------- 18 | oncodeChange(sql: string) { 19 | this.sql = sql; 20 | store.dispatch(ACTION.INPUT_SQL, this.sql); 21 | } 22 | 23 | submit() { 24 | store.dispatch(ACTION.SUBMIT); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/components/ResultTable.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Component from 'vue-class-component'; 3 | import { Prop } from 'vue-property-decorator'; 4 | import { ACTION } from '../store/action-types'; 5 | import store from '../store'; 6 | 7 | @Component 8 | export default class ResultTable extends Vue { 9 | @Prop() 10 | readonly sequence: number; 11 | 12 | @Prop() 13 | readonly sql: string; 14 | 15 | @Prop() 16 | readonly mode: number; 17 | 18 | @Prop() 19 | readonly data: any[][]; 20 | 21 | // --- method ---------------------------------------------- 22 | cancel(event: Event) { 23 | const id: number = Number(event.srcElement.id.split('-')[1]); 24 | store.dispatch(ACTION.CANCEL, id); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/components/Editor.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/controller/SqlController.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.controller 2 | 3 | import info.matsumana.tsujun.model.Request 4 | import info.matsumana.tsujun.model.ResponseTable 5 | import info.matsumana.tsujun.service.KsqlService 6 | import org.springframework.web.bind.annotation.PostMapping 7 | import org.springframework.web.bind.annotation.RequestBody 8 | import org.springframework.web.bind.annotation.RequestMapping 9 | import org.springframework.web.bind.annotation.RestController 10 | import reactor.core.publisher.Flux 11 | 12 | @RestController 13 | @RequestMapping("/") 14 | class SqlController(private val ksqlService: KsqlService) { 15 | 16 | @PostMapping("sql") 17 | fun sql(@RequestBody request: Request): Flux { 18 | return ksqlService.execute(request) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/tsujun 5 | 6 | docker: 7 | - image: matsumana/tsujun-primary:0.1.2 8 | 9 | steps: 10 | - checkout 11 | 12 | - restore_cache: 13 | keys: 14 | - tsujun-{{ checksum "build.gradle" }}-{{ checksum "src/main/resources/static/yarn.lock" }} 15 | - tsujun-{{ checksum "build.gradle" }} 16 | - tsujun 17 | 18 | - run: ./yarnInstall.sh 19 | - run: ./yarnLint.sh 20 | - run: ./yarnTest.sh 21 | - run: ./gradlew build 22 | 23 | - save_cache: 24 | paths: 25 | - /usr/local/share/.cache/yarn/v1 26 | - ~/.gradle 27 | key: tsujun-{{ checksum "build.gradle" }}-{{ checksum "src/main/resources/static/yarn.lock" }} 28 | 29 | - store_test_results: 30 | path: build/test-results 31 | - store_artifacts: 32 | path: build/libs/tsujun-*.jar 33 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/components/ResultTable.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/model/ResponseTable.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.model 2 | 3 | import java.util.* 4 | 5 | data class ResponseTable(val sequence: Int, 6 | val sql: String, 7 | val mode: Int, 8 | val data: Array) { 9 | 10 | override fun equals(other: Any?): Boolean { 11 | if (this === other) return true 12 | if (javaClass != other?.javaClass) return false 13 | 14 | other as ResponseTable 15 | 16 | if (sequence != other.sequence) return false 17 | if (sql != other.sql) return false 18 | if (mode != other.mode) return false 19 | if (!Arrays.equals(data, other.data)) return false 20 | 21 | return true 22 | } 23 | 24 | override fun hashCode(): Int { 25 | var result = sequence 26 | result = 31 * result + sql.hashCode() 27 | result = 31 * result + mode 28 | result = 31 * result + Arrays.hashCode(data) 29 | return result 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionContext, ActionTree } from 'vuex'; 2 | import { MUTATION } from './mutation-types'; 3 | import { ACTION } from './action-types'; 4 | import { Api } from '../api'; 5 | import { State } from './State'; 6 | import { ResponseBase } from './model/ResponseBase'; 7 | import { ResponseTransferObject } from './model/ResponseTransferObject'; 8 | 9 | const api = new Api(); 10 | 11 | const actions = > { 12 | [ACTION.INPUT_SQL](store: ActionContext, sql: string) { 13 | store.commit(MUTATION.INPUT_SQL, sql); 14 | }, 15 | [ACTION.SUBMIT](store: ActionContext) { 16 | { 17 | const response: ResponseBase = { 18 | sequence: store.state.sequence, 19 | sql: store.state.sql, 20 | mode: -1, 21 | }; 22 | store.commit(MUTATION.SUBMIT, response); 23 | } 24 | 25 | api.submit(store.state.sequence, store.state.sql, (data: ResponseTransferObject) => { 26 | store.commit(MUTATION.ON_RESPONSE, data); 27 | }); 28 | 29 | store.commit(MUTATION.SUBMITED); 30 | }, 31 | [ACTION.CANCEL](store: ActionContext, id: number) { 32 | store.commit(MUTATION.CANCEL, id); 33 | }, 34 | }; 35 | 36 | export default actions; 37 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/controller/ControllerAdvisor.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.controller 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.fasterxml.jackson.module.kotlin.KotlinModule 6 | import com.fasterxml.jackson.module.kotlin.readValue 7 | import info.matsumana.tsujun.exception.KsqlException 8 | import info.matsumana.tsujun.model.ResponseError 9 | import info.matsumana.tsujun.model.ksql.KsqlResponseErrorMessage 10 | import org.springframework.http.ResponseEntity 11 | import org.springframework.web.bind.annotation.ControllerAdvice 12 | import org.springframework.web.bind.annotation.ExceptionHandler 13 | 14 | @ControllerAdvice 15 | class ControllerAdvisor { 16 | 17 | companion object { 18 | private val mapper = ObjectMapper().registerModule(KotlinModule()) 19 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 20 | } 21 | 22 | @ExceptionHandler(KsqlException::class) 23 | fun handleKsqlException(e: KsqlException): ResponseEntity { 24 | val errorMessage = mapper.readValue(e.message) 25 | return ResponseEntity 26 | .status(e.statusCode) 27 | .body(ResponseError(e.sequence, e.sql, errorMessage.message)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsujun", 3 | "license": "Apache-2.0", 4 | "scripts": { 5 | "watch": "webpack -w --colors", 6 | "lint": "tslint --format stylish './javascripts/**/*.ts'", 7 | "build": "webpack --colors", 8 | "build:prod": "NODE_ENV=production webpack", 9 | "test": "ts-mocha -p ./javascripts/test --compilers ts:espower-typescript/guess ./**/*.spec.ts" 10 | }, 11 | "directories": { 12 | "test": "./javascripts/test" 13 | }, 14 | "dependencies": { 15 | "bulma": "^0.6.1", 16 | "lodash": "^4.17.4", 17 | "vue": "^2.5.2", 18 | "vue-bulma-brace": "^0.1.0", 19 | "vuex": "^3.0.1" 20 | }, 21 | "devDependencies": { 22 | "@types/lodash": "^4.14.85", 23 | "@types/mocha": "^2.2.44", 24 | "@types/power-assert": "^1.4.29", 25 | "css-loader": "^0.28.7", 26 | "espower-typescript": "^8.1.2", 27 | "mocha": "^4.0.1", 28 | "node-sass": "^4.6.1", 29 | "power-assert": "^1.4.4", 30 | "sass-loader": "^6.0.6", 31 | "ts-loader": "^2.3.7", 32 | "ts-mocha": "^1.0.3", 33 | "tslint": "^5.7.0", 34 | "tslint-config-airbnb": "^5.3.0", 35 | "tsutils": "^2.12.1", 36 | "typescript": "^2.6.1", 37 | "uglify-es": "^3.1.9", 38 | "uglify-es-webpack-plugin": "^0.10.0", 39 | "vue-class-component": "^6.1.0", 40 | "vue-loader": "^13.3.0", 41 | "vue-property-decorator": "^6.0.0", 42 | "vue-template-compiler": "^2.5.2", 43 | "watch": "^1.0.2", 44 | "webpack": "^3.7.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/test/store/mutations.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'power-assert'; 2 | import { ResponseTableRow } from '../../store/model/ResponseTableRow'; 3 | import { ResponseText } from '../../store/model/ResponseText'; 4 | 5 | describe('mutations', () => { 6 | it('convert from CREATE response', () => { 7 | const json: string = `{ 8 | "mode": 0, 9 | "sequence": 10, 10 | "sql": "CREATE STREAM pageviews_xx AS SELECT userid FROM pageviews", 11 | "text": "Message\\n----------------------------\\nStream created and running" 12 | }`; 13 | 14 | const jsonObj = JSON.parse(json) as ResponseText; 15 | 16 | assert.equal(jsonObj.mode, 0); 17 | assert.equal(jsonObj.sequence, 10); 18 | assert.equal(jsonObj.sql, 'CREATE STREAM pageviews_xx AS SELECT userid FROM pageviews'); 19 | assert.equal(jsonObj.text, 'Message\n----------------------------\nStream created and running'); 20 | }); 21 | 22 | it('convert from SELECT response', () => { 23 | const json: string = `{ 24 | "mode": 1, 25 | "sequence": 11, 26 | "sql": "SELECT * FROM pageviews_female LIMIT 3", 27 | "data": [ "aaa1", "bbb1", "ccc1", "ddd1" ] 28 | }`; 29 | 30 | const jsonObj = JSON.parse(json) as ResponseTableRow; 31 | 32 | assert.equal(jsonObj.mode, 1); 33 | assert.equal(jsonObj.sequence, 11); 34 | assert.equal(jsonObj.sql, 'SELECT * FROM pageviews_female LIMIT 3'); 35 | assert.equal(jsonObj.data.length, 4); 36 | assert.equal(jsonObj.data[0], 'aaa1'); 37 | assert.equal(jsonObj.data[1], 'bbb1'); 38 | assert.equal(jsonObj.data[2], 'ccc1'); 39 | assert.equal(jsonObj.data[3], 'ddd1'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Request as Req } from '../store/model/Request'; 2 | import { ResponseTransferObject } from '../store/model/ResponseTransferObject'; 3 | import { UserCancelError } from '../store/error/UserCancelError'; 4 | 5 | export class Api { 6 | 7 | submit(sequence: number, sql: string, callback: (data: ResponseTransferObject) => void) { 8 | const requestBody = new Req(); 9 | requestBody.sequence = sequence; 10 | requestBody.sql = sql; 11 | 12 | const headers = new Headers({ 13 | Accept: 'application/stream+json', // for streaming with WebFlux 14 | 'Content-Type': 'application/json', 15 | 'X-Requested-With': 'XMLHttpRequest', 16 | }); 17 | 18 | // referred to the follows. 19 | // https://www.chromestatus.com/feature/5804334163951616 20 | // https://googlechrome.github.io/samples/fetch-api/fetch-response-stream.html 21 | fetch('/sql', { 22 | method: 'POST', 23 | headers, 24 | body: JSON.stringify(requestBody), 25 | }).then(response => { 26 | if (response.ok) { 27 | return this.pump(response.body.getReader(), callback); 28 | } else { 29 | response.json() 30 | .then(value => { 31 | const obj: ResponseTransferObject = { 32 | sequence: value.sequence, 33 | payload: null, 34 | errorMessage: value.message, 35 | }; 36 | callback(obj); 37 | }); 38 | } 39 | }); 40 | } 41 | 42 | pump(reader: ReadableStreamReader, callback: (data: ResponseTransferObject) => void) { 43 | reader.read().then( 44 | (result) => { 45 | if (result.done) { 46 | return; 47 | } 48 | 49 | const rows = String.fromCharCode.apply('', new Uint16Array(result.value)); 50 | const obj: ResponseTransferObject = { 51 | sequence: -1, 52 | payload: rows, 53 | errorMessage: null, 54 | }; 55 | 56 | try { 57 | callback(obj); 58 | } catch (err) { 59 | if (err instanceof UserCancelError) { 60 | reader.cancel(); 61 | return; 62 | } 63 | throw err; 64 | } 65 | 66 | return this.pump(reader, callback); 67 | }, 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/resources/static/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const UglifyEsPlugin = require('uglify-es-webpack-plugin'); 3 | 4 | let jsOutputPath; 5 | if (process.env.JS_OUTPUT_PATH === null || process.env.JS_OUTPUT_PATH === undefined) { 6 | jsOutputPath = '../../../../build/resources/main/static/javascripts'; 7 | } else { 8 | jsOutputPath = process.env.JS_OUTPUT_PATH; 9 | } 10 | 11 | module.exports = { 12 | entry: "./javascripts/index.ts", 13 | output: { 14 | path: `${__dirname}/${jsOutputPath}`, 15 | filename: 'bundle.js' 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.vue$/, 21 | loader: 'vue-loader', 22 | options: { 23 | loaders: { 24 | // Since sass-loader (weirdly) has SCSS as its default parse mode, we map 25 | // the "scss" and "sass" values for the lang attribute to the right configs here. 26 | // other preprocessors should work out of the box, no loader config like this necessary. 27 | 'scss': 'vue-style-loader!css-loader!sass-loader', 28 | 'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax', 29 | } 30 | // other vue-loader options go here 31 | } 32 | }, 33 | { 34 | test: /\.tsx?$/, 35 | loader: 'ts-loader', 36 | exclude: /node_modules/, 37 | options: { 38 | appendTsSuffixTo: [/\.vue$/], 39 | } 40 | }, 41 | { 42 | test: /\.(png|jpg|gif|svg)$/, 43 | loader: 'file-loader', 44 | options: { 45 | name: '[name].[ext]?[hash]' 46 | } 47 | } 48 | ] 49 | }, 50 | resolve: { 51 | extensions: ['.ts', '.js', '.vue', '.json'], 52 | alias: { 53 | 'vue$': 'vue/dist/vue.esm.js' 54 | } 55 | }, 56 | performance: { 57 | hints: false 58 | }, 59 | devtool: '#eval-source-map' 60 | }; 61 | 62 | if (process.env.NODE_ENV === 'production') { 63 | module.exports.devtool = false; 64 | 65 | // http://vue-loader.vuejs.org/en/workflow/production.html 66 | module.exports.plugins = (module.exports.plugins || []).concat([ 67 | new webpack.DefinePlugin({ 68 | 'process.env': { 69 | NODE_ENV: '"production"' 70 | } 71 | }), 72 | new UglifyEsPlugin(), 73 | new webpack.LoaderOptionsPlugin({ 74 | minimize: true 75 | }) 76 | ]) 77 | } 78 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tsūjun 2 | 3 | [![CircleCI](https://circleci.com/gh/matsumana/tsujun.svg?style=shield)](https://circleci.com/gh/matsumana/tsujun) 4 | 5 | Tsūjun is yet another Web UI for [KSQL](https://github.com/confluentinc/ksql). 6 | 7 | ![](https://i.gyazo.com/37254dd6d69b6199e6436e4017dfd9c8.png) 8 | 9 | # Supporting KSQL syntax 10 | 11 | - SELECT 12 | - (LIST | SHOW) QUERIES 13 | - (LIST | SHOW) STREAMS 14 | - (LIST | SHOW) TABLES 15 | 16 | __*Other syntax will be supported in future version__ 17 | 18 | # Tested browsers 19 | 20 | - Safari 21 | - Chrome 22 | - Firefox 23 | 24 | __Caution__ 25 | 26 | This application is using [Fetch API](https://caniuse.com/#feat=fetch) and Fetch API's Readable streams. 27 | But in Firefox, this feature disabled by default. 28 | It can be enabled in `about:config`. 29 | 30 | ref: 31 | [Fetch API - Browser compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility) 32 | 33 | # How to setup local dev environment 34 | 35 | - Install Node.js 36 | - Install yarn 37 | 38 | ``` 39 | $ npm install -g yarn 40 | ``` 41 | 42 | - Install dependent javascript libraries 43 | 44 | ``` 45 | $ yarnInstall.sh 46 | ``` 47 | 48 | # How to launch on local 49 | 50 | Specify the your KSQL server with the environment variable `KSQL_SERVER` 51 | 52 | If the environment variable `KSQL_SERVER` is not set, it will connect to `http://localhost:8080` 53 | 54 | ## launch with Gradle 55 | 56 | ``` 57 | $ KSQL_SERVER=http://your_ksql_server ./gradlew bootRun 58 | ``` 59 | 60 | ## launch with IntelliJ 61 | 62 | ![](https://i.gyazo.com/cb65ef7cf4964d6dcb3e34f3ec4d400f.jpg) 63 | 64 | ## build javascript sources 65 | 66 | After launch the application, you must build javascript sources with an another terminal. 67 | 68 | Since output directories are different between Gradle and IntelliJ, please use the following scripts. 69 | 70 | ### for Gradle 71 | 72 | output directory is `build/resources/main/static/javascripts` 73 | 74 | ``` 75 | $ ./yarnWatchGradle.sh 76 | ``` 77 | 78 | ### for IntelliJ 79 | 80 | output directory is `out/production/resources/static/javascripts` 81 | 82 | ``` 83 | $ ./yarnWatchIntelliJ.sh 84 | ``` 85 | 86 | # How to build for production 87 | 88 | ``` 89 | $ ./gradlew clean build 90 | ``` 91 | 92 | # How to launch on production 93 | 94 | ``` 95 | $ KSQL_SERVER=http://your_ksql_server java -jar /path/to/tsujun-0.0.1.jar 96 | ``` 97 | 98 | # How to launch with Docker 99 | 100 | ``` 101 | $ docker run -p 8080:8080 -e KSQL_SERVER=http://your_ksql_server matsumana/tsujun:0.0.1 102 | ``` 103 | 104 | # Appendix 105 | 106 | Q: What is Tsūjun's the origin of the name? 107 | A: [Tsūjun Bridge](https://en.wikipedia.org/wiki/Ts%C5%ABjun_Bridge) 108 | -------------------------------------------------------------------------------- /src/main/resources/static/javascripts/store/mutations.ts: -------------------------------------------------------------------------------- 1 | import { MutationTree } from 'vuex'; 2 | import * as _ from 'lodash'; 3 | import { MUTATION } from './mutation-types'; 4 | import { State } from './State'; 5 | import { ResponseBase } from './model/ResponseBase'; 6 | import { ResponseText } from './model/ResponseText'; 7 | import { ResponseTable } from './model/ResponseTable'; 8 | import { ResponseTableRow } from './model/ResponseTableRow'; 9 | import { ResponseTransferObject } from './model/ResponseTransferObject'; 10 | import { UserCancelError } from './error/UserCancelError'; 11 | 12 | const mutations = > { 13 | [MUTATION.INPUT_SQL](state: State, sql: string) { 14 | state.sql = sql; 15 | }, 16 | [MUTATION.SUBMIT](state: State, response: ResponseBase) { 17 | state.results.unshift(response); 18 | }, 19 | [MUTATION.SUBMITED](state: State) { 20 | state.sequence = state.sequence + 1; 21 | }, 22 | [MUTATION.ON_RESPONSE](state: State, responseTransferObject: ResponseTransferObject) { 23 | if (responseTransferObject.errorMessage !== null) { 24 | for (const row of state.results) { 25 | if (row.sequence === responseTransferObject.sequence) { 26 | row.mode = 0; 27 | (row as ResponseText).text = responseTransferObject.errorMessage; 28 | } 29 | } 30 | } else { 31 | const json = responseTransferObject.payload; 32 | const responseRows = json.split(/\n/); 33 | 34 | for (const responseRow of responseRows) { 35 | if (responseRow === '') { 36 | continue; 37 | } 38 | 39 | const response: ResponseBase = JSON.parse(responseRow); 40 | 41 | for (const row of state.results) { 42 | if (row.sequence === response.sequence) { 43 | 44 | if (state.cancels.has(response.sequence)) { 45 | state.cancels.delete(response.sequence); 46 | throw new UserCancelError('Canceled by user'); 47 | } 48 | 49 | // apply response data to screen 50 | row.mode = response.mode; 51 | if (row.mode === 0) { 52 | // text 53 | (row as ResponseText).text = (response as ResponseText).text; 54 | } else { 55 | // table 56 | const responseTable = row as ResponseTable; 57 | if (responseTable.data === undefined) { 58 | responseTable.data = []; 59 | } 60 | responseTable.data.push((response as ResponseTableRow).data); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | // FIXME Since update of ResponseTable is not detected by vue.js, deepcopy and forcibly reflect it 68 | state.results = _.cloneDeep(state.results); 69 | }, 70 | [MUTATION.CANCEL](state: State, id: number) { 71 | state.cancels.add(id); 72 | }, 73 | }; 74 | 75 | export default mutations; 76 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/main/kotlin/info/matsumana/tsujun/service/KsqlService.kt: -------------------------------------------------------------------------------- 1 | package info.matsumana.tsujun.service 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.fasterxml.jackson.module.kotlin.KotlinModule 6 | import com.fasterxml.jackson.module.kotlin.readValue 7 | import info.matsumana.tsujun.config.KsqlServerConfig 8 | import info.matsumana.tsujun.exception.KsqlException 9 | import info.matsumana.tsujun.model.Request 10 | import info.matsumana.tsujun.model.ResponseTable 11 | import info.matsumana.tsujun.model.ksql.* 12 | import org.slf4j.LoggerFactory 13 | import org.springframework.http.HttpStatus 14 | import org.springframework.stereotype.Service 15 | import org.springframework.web.reactive.function.client.WebClient 16 | import org.springframework.web.reactive.function.client.WebClientResponseException 17 | import reactor.core.publisher.Flux 18 | import reactor.core.publisher.Mono 19 | import java.io.IOException 20 | import java.util.* 21 | 22 | @Service 23 | class KsqlService(private val ksqlServerConfig: KsqlServerConfig) { 24 | 25 | companion object { 26 | private val logger = LoggerFactory.getLogger(this::class.java.enclosingClass) 27 | private val mapper = ObjectMapper().registerModule(KotlinModule()) 28 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 29 | private val REGEX_SELECT = Regex("""^SELECT\s+?.*""", RegexOption.IGNORE_CASE) 30 | private val REGEX_QUERIES = Regex("""^(LIST|SHOW)\s+?QUERIES(\s|;)*""", RegexOption.IGNORE_CASE) 31 | private val REGEX_STREAMS = Regex("""^(LIST|SHOW)\s+?STREAMS(\s|;)*""", RegexOption.IGNORE_CASE) 32 | private val REGEX_TABLES = Regex("""^(LIST|SHOW)\s+?TABLES(\s|;)*""", RegexOption.IGNORE_CASE) 33 | private val emptyResponseSelect = KsqlResponseSelect(KsqlResponseSelectColumns(arrayOf())) 34 | private val emptyResponseQueries = 35 | arrayOf(KsqlResponseQueries(KsqlResponseQueriesInner("", arrayOf(KsqlResponseQueriesInnerQueries(KsqlResponseQueriesInnerQueriesId(""), "", ""))))) 36 | private val emptyResponseStreams = 37 | arrayOf(KsqlResponseStreams(KsqlResponseStreamsInner("", arrayOf(KsqlResponseStreamsInnerStreams("", "", ""))))) 38 | private val emptyResponseTables = 39 | arrayOf(KsqlResponseTables(KsqlResponseTablesInner("", arrayOf(KsqlResponseTablesInnerTables("", "", "", false))))) 40 | } 41 | 42 | fun execute(request: Request): Flux { 43 | logger.debug("KSQL server={}", ksqlServerConfig) 44 | 45 | // see aslo: 46 | // https://github.com/confluentinc/ksql/blob/master/ksql-parser/src/main/antlr4/io/confluent/ksql/parser/SqlBase.g4 47 | val sql = request.sql.trimStart().trimEnd() 48 | if (REGEX_SELECT.matches(sql)) { 49 | return select(request) 50 | } else if (REGEX_QUERIES.matches(sql)) { 51 | return queries(request) 52 | } else if (REGEX_STREAMS.matches(sql)) { 53 | return streams(request) 54 | } else if (REGEX_TABLES.matches(sql)) { 55 | return tables(request) 56 | } else { 57 | val rawMessage = """Currently, Tsūjun supports only the following syntax. 58 | | 59 | | - SELECT 60 | | - (LIST | SHOW) QUERIES 61 | | - (LIST | SHOW) STREAMS 62 | | - (LIST | SHOW) TABLES 63 | """.trimMargin() 64 | val message = mapper.writeValueAsString(KsqlResponseErrorMessage(rawMessage, emptyList())) 65 | throw KsqlException(request.sequence, request.sql, HttpStatus.BAD_REQUEST.value(), message) 66 | } 67 | } 68 | 69 | private fun select(request: Request): Flux { 70 | // In the WebClient, sometimes the response gets cut off in the middle of json. 71 | // For example, as in the following log. 72 | // So, temporarily save the failed data to a variable, combine and use the next response. 73 | // 74 | // 2017-12-28 20:54:27.832 DEBUG 70874 --- [ctor-http-nio-5] i.matsumana.tsujun.service.KsqlService : {"r 75 | // 2017-12-28 20:54:27.833 DEBUG 70874 --- [ctor-http-nio-5] i.matsumana.tsujun.service.KsqlService : ow":{"columns":["Page_27",0]},"errorMessage":null} 76 | val previousFailed = ArrayDeque() 77 | 78 | return WebClient.create(ksqlServerConfig.server) 79 | .post() 80 | .uri("/query") 81 | .body(Mono.just(KsqlRequest(request.sql)), KsqlRequest::class.java) 82 | .retrieve() 83 | .bodyToFlux(String::class.java) 84 | .doOnError { e -> 85 | handleException(e, request) 86 | } 87 | .map { orgString -> 88 | logger.debug(orgString) 89 | 90 | val s = orgString.trim() 91 | if (s.isEmpty()) { 92 | emptyResponseSelect 93 | } else { 94 | try { 95 | mapper.readValue(s) 96 | } catch (ignore: IOException) { 97 | if (previousFailed.isEmpty()) { 98 | previousFailed.addFirst(s) 99 | emptyResponseSelect 100 | } else { 101 | try { 102 | val completeJson = previousFailed.removeFirst() + s 103 | mapper.readValue(completeJson) 104 | } catch (ignore: IOException) { 105 | emptyResponseSelect 106 | } 107 | } 108 | } 109 | } 110 | } 111 | .filter { res -> !res.row.columns.isEmpty() } 112 | .map { res -> 113 | ResponseTable(sequence = request.sequence, 114 | sql = request.sql, 115 | mode = 1, 116 | data = res.row.columns) 117 | } 118 | } 119 | 120 | private fun queries(request: Request): Flux { 121 | return generateWebClientKsql(request) 122 | .map { orgString -> 123 | logger.debug(orgString) 124 | 125 | val s = orgString.trim() 126 | if (s.isEmpty()) { 127 | emptyResponseQueries 128 | } else { 129 | try { 130 | mapper.readValue>(s) 131 | } catch (ignore: IOException) { 132 | emptyResponseQueries 133 | } 134 | } 135 | } 136 | .map { res -> res.get(0) } 137 | .filter { res -> !res.queries.queries.isEmpty() } 138 | .flatMap { res -> 139 | val header = ResponseTable(sequence = request.sequence, 140 | sql = request.sql, 141 | mode = 1, 142 | data = arrayOf("ID", "Kafka Topic", "Query String")) 143 | 144 | val data = res.queries.queries.map { m -> 145 | ResponseTable(sequence = request.sequence, 146 | sql = request.sql, 147 | mode = 1, 148 | data = arrayOf(m.id.id, m.kafkaTopic, m.queryString)) 149 | } 150 | 151 | val list = mutableListOf(header) 152 | list.addAll(data) 153 | Flux.fromIterable(list) 154 | } 155 | } 156 | 157 | private fun streams(request: Request): Flux { 158 | return generateWebClientKsql(request) 159 | .map { orgString -> 160 | logger.debug(orgString) 161 | 162 | val s = orgString.trim() 163 | if (s.isEmpty()) { 164 | emptyResponseStreams 165 | } else { 166 | try { 167 | mapper.readValue>(s) 168 | } catch (ignore: IOException) { 169 | emptyResponseStreams 170 | } 171 | } 172 | } 173 | .map { res -> res.get(0) } 174 | .filter { res -> !res.streams.streams.isEmpty() } 175 | .flatMap { res -> 176 | val header = ResponseTable(sequence = request.sequence, 177 | sql = request.sql, 178 | mode = 1, 179 | data = arrayOf("Stream Name", "Ksql Topic", "Format")) 180 | 181 | val data = res.streams.streams.map { m -> 182 | ResponseTable(sequence = request.sequence, 183 | sql = request.sql, 184 | mode = 1, 185 | data = arrayOf(m.name, m.topic, m.format)) 186 | } 187 | 188 | val list = mutableListOf(header) 189 | list.addAll(data) 190 | Flux.fromIterable(list) 191 | } 192 | } 193 | 194 | private fun tables(request: Request): Flux { 195 | return generateWebClientKsql(request) 196 | .map { orgString -> 197 | logger.debug(orgString) 198 | 199 | val s = orgString.trim() 200 | if (s.isEmpty()) { 201 | emptyResponseTables 202 | } else { 203 | try { 204 | mapper.readValue>(s) 205 | } catch (ignore: IOException) { 206 | emptyResponseTables 207 | } 208 | } 209 | } 210 | .map { res -> res.get(0) } 211 | .filter { res -> !res.tables.tables.isEmpty() } 212 | .flatMap { res -> 213 | val header = ResponseTable(sequence = request.sequence, 214 | sql = request.sql, 215 | mode = 1, 216 | data = arrayOf("Stream Name", "Ksql Topic", "Format", "Windowed")) 217 | 218 | val data = res.tables.tables.map { m -> 219 | ResponseTable(sequence = request.sequence, 220 | sql = request.sql, 221 | mode = 1, 222 | data = arrayOf(m.name, m.topic, m.format, m.isWindowed)) 223 | } 224 | 225 | val list = mutableListOf(header) 226 | list.addAll(data) 227 | Flux.fromIterable(list) 228 | } 229 | } 230 | 231 | private fun generateWebClientKsql(request: Request): Flux { 232 | return WebClient.create(ksqlServerConfig.server) 233 | .post() 234 | .uri("/ksql") 235 | .body(Mono.just(KsqlRequest(request.sql)), KsqlRequest::class.java) 236 | .retrieve() 237 | .bodyToFlux(String::class.java) 238 | .doOnError { e -> 239 | handleException(e, request) 240 | } 241 | } 242 | 243 | private fun handleException(e: Throwable, request: Request) { 244 | logger.info("WebClient Error", e) 245 | 246 | if (e is WebClientResponseException) { 247 | throw KsqlException(request.sequence, request.sql, e.statusCode.value(), e.responseBodyAsString) 248 | } else { 249 | throw e 250 | } 251 | } 252 | } 253 | --------------------------------------------------------------------------------