diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f8047df..c53d890 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,8 @@ async-embedded = "0.0.10" session-embedded = "0.0.10" storage-embedded = "0.1.16" +kafka-client-service = "0.0.18" + [libraries] common-api = { module = "com.github.sibdevtools:api-common", version.ref = "common-api" } localization-api = { module = "com.github.sibdevtools:api-localization", version.ref = "localization-api" } @@ -35,6 +37,8 @@ async-embedded = { module = "com.github.sibdevtools:service-async-embedded", ver session-embedded = { module = "com.github.sibdevtools:service-session-embedded", version.ref = "session-embedded" } storage-embedded = { module = "com.github.sibdevtools:service-storage-embedded", version.ref = "storage-embedded" } +kafka-client-service = { module = "com.github.sibdevtools:service-kafka-client", version.ref = "kafka-client-service" } + spring-openapi-starter = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "spring-openapi-doc" } graalvm-api = { module = "org.graalvm.truffle:truffle-api", version.ref = "graalvm" } diff --git a/web-app-frontend/src/components/GraalVMMockContent.tsx b/web-app-frontend/src/components/GraalVMMockContent.tsx index ce99788..f20c1e7 100644 --- a/web-app-frontend/src/components/GraalVMMockContent.tsx +++ b/web-app-frontend/src/components/GraalVMMockContent.tsx @@ -4,13 +4,13 @@ import AceEditor from 'react-ace'; import '../const/ace.imports'; import { loadSettings } from '../settings/utils'; -import { Button, ButtonGroup, Form, Table } from 'react-bootstrap'; -import CodeDocumentation from '../pages/mock/CodeDocumentation'; +import { Button, ButtonGroup, Form } from 'react-bootstrap'; export interface GraalVMMockContentProps { mode: 'javascript' | 'python'; content: ArrayBuffer; setContent: (content: ArrayBuffer) => void; + disabled?: boolean; } const textEncoder = new TextEncoder(); @@ -18,6 +18,7 @@ const GraalVMMockContent: React.FC = ({ mode, content, setContent, + disabled, }) => { const settings = loadSettings(); const [isWordWrapEnabled, setIsWordWrapEnabled] = useState(true); @@ -56,6 +57,7 @@ const GraalVMMockContent: React.FC = ({ fontSize={14} width="100%" height="480px" + readOnly={disabled} showPrintMargin={true} showGutter={true} highlightActiveLine={true} diff --git a/web-app-frontend/src/components/HttpHeadersForm.tsx b/web-app-frontend/src/components/HttpHeadersForm.tsx index 4703dbe..3938a53 100644 --- a/web-app-frontend/src/components/HttpHeadersForm.tsx +++ b/web-app-frontend/src/components/HttpHeadersForm.tsx @@ -11,6 +11,7 @@ interface Header { interface HttpHeadersFormProps { meta: { [key: string]: string }; setMeta: (newMeta: { [key: string]: string }) => void; + disabled?: boolean; } const commonHeaders = [ @@ -31,7 +32,11 @@ const headerValueSuggestions: { [key: string]: string[] } = { 'Cache-Control': ['no-cache', 'no-store', 'must-revalidate', 'public', 'private'], }; -const HttpHeadersForm: React.FC = ({ meta, setMeta }) => { +const HttpHeadersForm: React.FC = ({ + meta, + setMeta, + disabled + }) => { const [notFilledExist, setNotFilledExist] = useState(false); const initialHeaders = meta['HTTP_HEADERS'] @@ -101,6 +106,7 @@ const HttpHeadersForm: React.FC = ({ meta, setMeta }) => { placeholder="Header Key" value={header.key} onChange={(e) => handleHeaderChange(index, 'key', e.target.value)} + disabled={disabled} /> {commonHeaders.map((headerName, i) => ( @@ -113,6 +119,7 @@ const HttpHeadersForm: React.FC = ({ meta, setMeta }) => { placeholder="Header Value" value={header.value} onChange={(e) => handleHeaderChange(index, 'value', e.target.value)} + disabled={disabled} /> {(headerValueSuggestions[header.key] || []).map((valueSuggestion, i) => ( @@ -122,7 +129,7 @@ const HttpHeadersForm: React.FC = ({ meta, setMeta }) => { + + {it.implementation} + + )} {'implementations' in it && ( - - {it.implementations[mode]} - +
+ + + {it.implementations[mode]} + +
)} diff --git a/web-app-frontend/src/pages/mock/MockForm.tsx b/web-app-frontend/src/pages/mock/MockForm.tsx index 4ae8c50..bdf7e98 100644 --- a/web-app-frontend/src/pages/mock/MockForm.tsx +++ b/web-app-frontend/src/pages/mock/MockForm.tsx @@ -1,11 +1,11 @@ -import React from 'react'; -import { Button, Col, Container, Form, InputGroup, OverlayTrigger, Row, Table, Tooltip } from 'react-bootstrap'; +import React, { useState } from 'react'; +import { Alert, Button, Col, Container, Form, InputGroup, Row, Table } from 'react-bootstrap'; import { methods, MockType, mockTypes, statusCodes } from '../../const/common.const'; import HttpHeadersForm from '../../components/HttpHeadersForm'; import StaticMockContent from '../../components/StaticMockContent'; import GraalVMMockContent from '../../components/GraalVMMockContent'; import StaticFileMockContent from '../../components/StaticFileMockContent'; -import { ArrowLeft01Icon, FloppyDiskIcon } from 'hugeicons-react'; +import { ArrowLeft01Icon, FloppyDiskIcon, InformationCircleIcon } from 'hugeicons-react'; import { Loader } from '../../components/Loader'; import './MockForm.css'; import CodeDocumentation from './CodeDocumentation'; @@ -18,6 +18,8 @@ type MockFormProps = { setModifyingMock: (value: ModifyingMock) => void; onSubmit: (e: React.FormEvent) => void; isEditMode: boolean; + saving: boolean; + saved: boolean; navigateBack: () => void; }; @@ -27,44 +29,16 @@ export const MockForm: React.FC = ({ setModifyingMock, onSubmit, isEditMode, + saving, + saved, navigateBack }) => { + const [showPathHint, setShowPathHint] = useState(false); const onMockTypeChange = (e: React.ChangeEvent) => { - setModifyingMock({...modifyingMock, type: e.target.value as MockType}) + setModifyingMock({ ...modifyingMock, type: e.target.value as MockType }); }; - const pathTooltip = ( - - - - - - - - - - - - - - - - - - - - - - - - - - -
WildcardDescription
?Matches exactly one character.
*Matches zero or more characters.
**Matches zero or more 'directories' in a path
{spring:[a-z]+}Matches regExp [a-z]+ as a path variable named "spring"
-
- ); - return ( @@ -98,8 +72,9 @@ export const MockForm: React.FC = ({ type="text" id={'nameInput'} value={modifyingMock.name} - onChange={(e) => setModifyingMock({...modifyingMock, name: e.target.value})} - required + onChange={(e) => setModifyingMock({ ...modifyingMock, name: e.target.value })} + required={true} + disabled={saving} /> @@ -113,8 +88,9 @@ export const MockForm: React.FC = ({ setModifyingMock({...modifyingMock, method: it.value})} + onChange={it => setModifyingMock({ ...modifyingMock, method: it.value })} required={true} + disabled={saving} suggestions={methods.map(it => { return { key: it, value: it }; })} @@ -128,22 +104,55 @@ export const MockForm: React.FC = ({ Path / - setModifyingMock({ ...modifyingMock, path: e.target.value })} + placeholder="Ant pattern or path" + required + disabled={saving} + /> + + + {/* Path Hint */} + {showPathHint && ( + + + + + + + + + + + + + + + + + + + + + + + + + + +
WildcardDescription
?Matches exactly one character
*Matches zero or more characters
**Matches zero or more 'directories' in a path
{spring:[a-z]+}Matches regExp [a-z]+ as a path variable named "spring"
+
+ )} @@ -151,8 +160,9 @@ export const MockForm: React.FC = ({ Http Headers setModifyingMock({...modifyingMock, meta: it})} + setMeta={it => setModifyingMock({ ...modifyingMock, meta: it })} /> @@ -170,11 +180,14 @@ export const MockForm: React.FC = ({ type={'number'} mode={'free'} value={modifyingMock.meta['STATUS_CODE']} - onChange={it => setModifyingMock({...modifyingMock, meta: { - ...modifyingMock.meta, + onChange={it => setModifyingMock({ + ...modifyingMock, meta: { + ...modifyingMock.meta, STATUS_CODE: `${it.key ?? it.value}` - }})} + } + })} required={true} + disabled={saving} suggestions={Array.from(statusCodes).map(([key, value]) => { return { key: `${key}`, value: `${key}: ${value}` }; })} @@ -196,8 +209,9 @@ export const MockForm: React.FC = ({ id={'delayInput'} min={0} value={`${modifyingMock.delay}`} - onChange={e => setModifyingMock({...modifyingMock, delay: +e.target.value})} - required + onChange={e => setModifyingMock({ ...modifyingMock, delay: +e.target.value })} + required={true} + disabled={saving} /> ms @@ -215,7 +229,8 @@ export const MockForm: React.FC = ({ id={'typeSelect'} value={modifyingMock.type} onChange={onMockTypeChange} - required + required={true} + disabled={saving} > { Array.from(mockTypes).map( @@ -234,40 +249,50 @@ export const MockForm: React.FC = ({ {/* Content Section Based on Mock Type */} {modifyingMock.type === 'STATIC' && ( setModifyingMock({...modifyingMock, content: it})} + setContent={it => setModifyingMock({ ...modifyingMock, content: it })} meta={modifyingMock.meta} - setMeta={it => setModifyingMock({...modifyingMock, meta: it})} + setMeta={it => setModifyingMock({ ...modifyingMock, meta: it })} creation={!isEditMode} /> )} {modifyingMock.type === 'STATIC_FILE' && setModifyingMock({...modifyingMock, content: it})} + setContent={it => setModifyingMock({ ...modifyingMock, content: it })} isEditMode={isEditMode} />} {(modifyingMock.type === 'JS' || modifyingMock.type === 'PYTHON') && ( setModifyingMock({...modifyingMock, content: it})} + setContent={it => setModifyingMock({ ...modifyingMock, content: it })} /> )} - {/* Submit Button */} + {/* Save Button */} + {saved && ( + + Saved successfully. + + )} + {(modifyingMock.type === 'JS' || modifyingMock.type === 'PYTHON') && ( )} diff --git a/web-app/build.gradle.kts b/web-app/build.gradle.kts index 2acc3bc..7539cf1 100644 --- a/web-app/build.gradle.kts +++ b/web-app/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { implementation("jakarta.annotation:jakarta.annotation-api") implementation("jakarta.persistence:jakarta.persistence-api") + implementation("org.apache.kafka:kafka-clients") + implementation(libs.bundles.graalvm) implementation(libs.mapstruct) @@ -43,6 +45,8 @@ dependencies { implementation(libs.bundles.service.api) + implementation(libs.kafka.client.service) + testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-web") diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/GraalVMConverter.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/GraalVMConverter.java index 77a0eaa..5f24818 100644 --- a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/GraalVMConverter.java +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/GraalVMConverter.java @@ -66,4 +66,23 @@ public static Map> convertSections(Map convertInput(Map input) { + if (input == null) { + return Collections.emptyMap(); + } + var serialized = new LinkedHashMap(); + + for (var attributeEntry : input.entrySet()) { + var key = attributeEntry.getKey(); + var value = attributeEntry.getValue(); + var serializedValue = toSerializable(value); + if (serializedValue == null) { + log.warn("Skipped unsupported input attribute '{}', type: {}", key, value.getClass().getName()); + continue; + } + serialized.put(key, serializedValue); + } + + return serialized; + } } diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/GraalVMRequestHandler.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/GraalVMRequestHandler.java index 01539f1..cd259ba 100644 --- a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/GraalVMRequestHandler.java +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/GraalVMRequestHandler.java @@ -1,15 +1,12 @@ package com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.sibdevtools.session.api.service.SessionService; import com.github.sibdevtools.storage.api.service.StorageService; import com.github.sibdevtools.web.app.mocks.entity.HttpMockEntity; import com.github.sibdevtools.web.app.mocks.service.handler.RequestHandler; import com.github.sibdevtools.web.app.mocks.service.handler.impl.CommonResponsePreparer; -import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.GraalVMMocksContext; -import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.GraalVMRequest; -import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.GraalVMResponse; -import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.GraalVMSessions; +import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.*; +import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.kafka.WebApplicationMocksGraalVMKafka; import jakarta.annotation.Nonnull; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -30,7 +27,8 @@ public abstract class GraalVMRequestHandler implements RequestHandler { protected final String language; protected final StorageService storageService; - protected final SessionService sessionService; + protected final WebApplicationMocksGraalVMSessions graalVMSessions; + protected final WebApplicationMocksGraalVMKafka graalVMKafka; protected final ObjectMapper objectMapper; protected final CommonResponsePreparer commonResponsePreparer; @@ -51,7 +49,8 @@ public void handle(@Nonnull String path, var context = GraalVMMocksContext.builder() .request(new GraalVMRequest(objectMapper, path, rq)) .response(new GraalVMResponse(objectMapper, rs)) - .sessions(new GraalVMSessions(sessionService)) + .sessions(graalVMSessions) + .kafka(graalVMKafka) .build(); try (var js = Context.newBuilder(language) diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/GraalVMMocksContext.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/GraalVMMocksContext.java index 11a5402..4efa4e8 100644 --- a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/GraalVMMocksContext.java +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/GraalVMMocksContext.java @@ -1,5 +1,6 @@ package com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto; +import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.kafka.WebApplicationMocksGraalVMKafka; import lombok.Builder; import org.graalvm.polyglot.HostAccess; @@ -10,5 +11,6 @@ @Builder public record GraalVMMocksContext(@HostAccess.Export GraalVMRequest request, @HostAccess.Export GraalVMResponse response, - @HostAccess.Export GraalVMSessions sessions) { + @HostAccess.Export WebApplicationMocksGraalVMSessions sessions, + @HostAccess.Export WebApplicationMocksGraalVMKafka kafka) { } diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/GraalVMSessions.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/WebApplicationMocksGraalVMSessions.java similarity index 95% rename from web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/GraalVMSessions.java rename to web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/WebApplicationMocksGraalVMSessions.java index e2d092c..36fa41c 100644 --- a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/GraalVMSessions.java +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/WebApplicationMocksGraalVMSessions.java @@ -9,6 +9,7 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.graalvm.polyglot.HostAccess; +import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; @@ -18,8 +19,9 @@ * @since 0.0.1 */ @Slf4j +@Component @AllArgsConstructor -public class GraalVMSessions { +public class WebApplicationMocksGraalVMSessions { private final SessionService sessionService; @HostAccess.Export diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/ConsumeMessagesRq.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/ConsumeMessagesRq.java new file mode 100644 index 0000000..df47d36 --- /dev/null +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/ConsumeMessagesRq.java @@ -0,0 +1,21 @@ +package com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.kafka; + +import java.util.List; + +/** + * @author sibmaks + * @since 0.0.22 + */ +public record ConsumeMessagesRq( + String groupCode, + List bootstrapServers, + String topic, + int maxMessages, + Integer maxTimeout, + Direction direction +) { + enum Direction { + EARLIEST, + LATEST + } +} diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/ConsumeMessagesRs.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/ConsumeMessagesRs.java new file mode 100644 index 0000000..e9c1302 --- /dev/null +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/ConsumeMessagesRs.java @@ -0,0 +1,17 @@ +package com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.kafka; + +import lombok.Builder; +import org.graalvm.polyglot.HostAccess; + +import java.util.List; + +/** + * @author sibmaks + * @since 0.0.22 + */ +@Builder +public record ConsumeMessagesRs( + @HostAccess.Export String topic, + @HostAccess.Export List messages +) { +} diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/ConsumedMessage.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/ConsumedMessage.java new file mode 100644 index 0000000..9e62d90 --- /dev/null +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/ConsumedMessage.java @@ -0,0 +1,172 @@ +package com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.kafka; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.sibdevtools.web.app.mocks.exception.UnexpectedErrorException; +import jakarta.annotation.Nullable; +import lombok.Builder; +import lombok.val; +import org.graalvm.polyglot.HostAccess; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; + +/** + * @author sibmaks + * @since 0.0.22 + */ +@Builder +public record ConsumedMessage( + ObjectMapper objectMapper, + @HostAccess.Export int partition, + @HostAccess.Export long offset, + @HostAccess.Export long timestamp, + @HostAccess.Export String timestampType, + @HostAccess.Export int serializedKeySize, + @HostAccess.Export int serializedValueSize, + @HostAccess.Export Map headers, + @HostAccess.Export byte[] key, + @HostAccess.Export byte[] value +) { + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + + val that = (ConsumedMessage) o; + return offset == that.offset && + partition == that.partition && + timestamp == that.timestamp && + serializedKeySize == that.serializedKeySize && + serializedValueSize == that.serializedValueSize && + Arrays.equals(key, that.key) && + Arrays.equals(value, that.value) && + Objects.equals(timestampType, that.timestampType) && + Objects.equals(headers, that.headers); + } + + @Override + public int hashCode() { + int result = partition; + result = 31 * result + Long.hashCode(offset); + result = 31 * result + Long.hashCode(timestamp); + result = 31 * result + Objects.hashCode(timestampType); + result = 31 * result + serializedKeySize; + result = 31 * result + serializedValueSize; + result = 31 * result + Objects.hashCode(headers); + result = 31 * result + Arrays.hashCode(key); + result = 31 * result + Arrays.hashCode(value); + return result; + } + + @Override + public String toString() { + return "ConsumedMessage{" + + "objectMapper=" + objectMapper + + ", partition=" + partition + + ", offset=" + offset + + ", timestamp=" + timestamp + + ", timestampType='" + timestampType + '\'' + + ", serializedKeySize=" + serializedKeySize + + ", serializedValueSize=" + serializedValueSize + + ", headers=" + headers + + ", key=" + Arrays.toString(key) + + ", value=" + Arrays.toString(value) + + '}'; + } + + /** + * Get key as string + * + * @return key + */ + @HostAccess.Export + public String textKey() { + return asText(key); + } + + /** + * Get key as JSON + * + * @return key + */ + @HostAccess.Export + public Object jsonKey() { + return asJson(textKey()); + } + + private Object asJson(String text) { + try { + var jsonNode = objectMapper.readTree(text); + + if (jsonNode.isArray()) { + return objectMapper.convertValue(jsonNode, Object[].class); + } else if (jsonNode.isObject()) { + return objectMapper.convertValue(jsonNode, Map.class); + } + return jsonNode; + } catch (IOException e) { + throw new UnexpectedErrorException("Can't write to response", e); + } + } + + /** + * Get value as string + * + * @return value + */ + @HostAccess.Export + public String textValue() { + return asText(value); + } + + private String asText(byte[] value) { + if (value == null) return null; + return new String(value, StandardCharsets.UTF_8); + } + + /** + * Get value as JSON + * + * @return value + */ + @HostAccess.Export + public Object jsonValue() { + return asJson(textValue()); + } + + /** + * Get header value as byte array + * + * @param key header key + * @return header value + */ + @Nullable + @HostAccess.Export + public byte[] header(String key) { + return headers.get(key); + } + + /** + * Get header value as UTF-8 string + * + * @param key header key + * @return header value + */ + @HostAccess.Export + public String headerText(String key) { + val value = header(key); + return asText(value); + } + + /** + * Get value as JSON + * + * @return value + */ + @HostAccess.Export + public Object headerJson(String key) { + return asJson(headerText(key)); + } +} diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/PublishMessageRq.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/PublishMessageRq.java new file mode 100644 index 0000000..11b827d --- /dev/null +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/PublishMessageRq.java @@ -0,0 +1,68 @@ +package com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.kafka; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * @author sibmaks + * @since 0.0.22 + */ +public record PublishMessageRq( + String groupCode, + List bootstrapServers, + String topic, + //asd + Integer partition, + Long timestamp, + byte[] key, + byte[] value, + Map headers, + Integer maxTimeout +) { + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + + PublishMessageRq that = (PublishMessageRq) o; + return Arrays.equals(key, that.key) && + topic.equals(that.topic) && + Arrays.equals(value, that.value) && + Objects.equals(timestamp, that.timestamp) && + Objects.equals(groupCode, that.groupCode) && + Objects.equals(partition, that.partition) && + Objects.equals(maxTimeout, that.maxTimeout) && + Objects.equals(headers, that.headers) && + Objects.equals(bootstrapServers, that.bootstrapServers); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(groupCode); + result = 31 * result + Objects.hashCode(bootstrapServers); + result = 31 * result + topic.hashCode(); + result = 31 * result + Objects.hashCode(partition); + result = 31 * result + Objects.hashCode(timestamp); + result = 31 * result + Arrays.hashCode(key); + result = 31 * result + Arrays.hashCode(value); + result = 31 * result + Objects.hashCode(headers); + result = 31 * result + Objects.hashCode(maxTimeout); + return result; + } + + @Override + public String toString() { + return "PublishMessageRq{" + + "groupCode='" + groupCode + '\'' + + ", bootstrapServers=" + bootstrapServers + + ", topic='" + topic + '\'' + + ", partition=" + partition + + ", timestamp=" + timestamp + + ", key=" + Arrays.toString(key) + + ", value=" + Arrays.toString(value) + + ", headers=" + headers + + ", maxTimeout=" + maxTimeout + + '}'; + } +} diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/PublishMessageRs.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/PublishMessageRs.java new file mode 100644 index 0000000..ecb37fa --- /dev/null +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/PublishMessageRs.java @@ -0,0 +1,16 @@ +package com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.kafka; + +import org.graalvm.polyglot.HostAccess; + +/** + * @author sibmaks + * @since 0.0.22 + */ +public record PublishMessageRs( + @HostAccess.Export long offset, + @HostAccess.Export long timestamp, + @HostAccess.Export int serializedKeySize, + @HostAccess.Export int serializedValueSize, + @HostAccess.Export int partition +) { +} diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/PublishTemplateMessageRq.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/PublishTemplateMessageRq.java new file mode 100644 index 0000000..2645a67 --- /dev/null +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/PublishTemplateMessageRq.java @@ -0,0 +1,68 @@ +package com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.kafka; + +import jakarta.validation.constraints.NotNull; + +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; + +/** + * @author sibmaks + * @since 0.0.22 + */ +public record PublishTemplateMessageRq( + String groupCode, + String topic, + String templateCode, + Integer partition, + Long timestamp, + byte[] key, + Map input, + Map headers, + Integer maxTimeout +) { + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + + PublishTemplateMessageRq that = (PublishTemplateMessageRq) o; + return Arrays.equals(key, that.key) && + topic.equals(that.topic) && + Objects.equals(timestamp, that.timestamp) && + Objects.equals(groupCode, that.groupCode) && + Objects.equals(partition, that.partition) && + Objects.equals(maxTimeout, that.maxTimeout) && + Objects.equals(templateCode, that.templateCode) && + Objects.equals(input, that.input) && + Objects.equals(headers, that.headers); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(groupCode); + result = 31 * result + topic.hashCode(); + result = 31 * result + Objects.hashCode(templateCode); + result = 31 * result + Objects.hashCode(partition); + result = 31 * result + Objects.hashCode(timestamp); + result = 31 * result + Arrays.hashCode(key); + result = 31 * result + Objects.hashCode(input); + result = 31 * result + Objects.hashCode(headers); + result = 31 * result + Objects.hashCode(maxTimeout); + return result; + } + + @Override + public String toString() { + return "PublishTemplateMessageRq{" + + "groupCode='" + groupCode + '\'' + + ", topic='" + topic + '\'' + + ", templateCode='" + templateCode + '\'' + + ", partition=" + partition + + ", timestamp=" + timestamp + + ", key=" + Arrays.toString(key) + + ", input=" + input + + ", headers=" + headers + + ", maxTimeout=" + maxTimeout + + '}'; + } +} diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/WebApplicationMocksGraalVMKafka.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/WebApplicationMocksGraalVMKafka.java new file mode 100644 index 0000000..f9df3a1 --- /dev/null +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/dto/kafka/WebApplicationMocksGraalVMKafka.java @@ -0,0 +1,173 @@ +package com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.kafka; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.sibdevtools.service.kafka.client.api.dto.RecordMetadataDto; +import com.github.sibdevtools.service.kafka.client.api.rq.SendTemplateMessageRq; +import com.github.sibdevtools.service.kafka.client.service.BootstrapGroupService; +import com.github.sibdevtools.service.kafka.client.service.MessageConsumerService; +import com.github.sibdevtools.service.kafka.client.service.MessagePublisherService; +import com.github.sibdevtools.service.kafka.client.service.TemplateMessageService; +import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.GraalVMConverter; +import jakarta.annotation.Nonnull; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.header.Headers; +import org.graalvm.polyglot.HostAccess; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author sibmaks + * @since 0.0.22 + */ +@Slf4j +@Component +@AllArgsConstructor +public class WebApplicationMocksGraalVMKafka { + protected final ObjectMapper objectMapper; + protected final BootstrapGroupService bootstrapGroupService; + protected final MessageConsumerService messageConsumerService; + protected final MessagePublisherService messagePublisherService; + protected final TemplateMessageService templateMessageService; + + private static PublishMessageRs getPublishMessageRs(RecordMetadataDto it) { + return new PublishMessageRs( + it.getOffset(), + it.getTimestamp(), + it.getSerializedKeySize(), + it.getSerializedValueSize(), + it.getPartition() + ); + } + + @HostAccess.Export + public PublishMessageRs publish(@Nonnull Object arg) { + var rq = objectMapper.convertValue(arg, PublishMessageRq.class); + var groupCode = rq.groupCode(); + if (groupCode != null) { + var bootstrapGroup = bootstrapGroupService.getByCode(groupCode); + return messagePublisherService.sendMessage( + bootstrapGroup.getId(), + rq.topic(), + rq.partition(), + rq.timestamp(), + rq.key(), + rq.value(), + rq.headers(), + rq.maxTimeout() + ) + .map(WebApplicationMocksGraalVMKafka::getPublishMessageRs) + .orElse(null); + } + return messagePublisherService.sendMessage( + rq.bootstrapServers(), + rq.topic(), + rq.partition(), + rq.timestamp(), + rq.key(), + rq.value(), + rq.headers(), + rq.maxTimeout() + ) + .map(WebApplicationMocksGraalVMKafka::getPublishMessageRs) + .orElse(null); + } + + @HostAccess.Export + public PublishMessageRs publishTemplate(@Nonnull Object arg) { + var rq = objectMapper.convertValue(arg, PublishTemplateMessageRq.class); + var bootstrapGroup = bootstrapGroupService.getByCode(rq.groupCode()); + var templateRsDto = templateMessageService.getByCode(rq.templateCode()); + return templateMessageService.send( + templateRsDto.getId(), + new SendTemplateMessageRq( + bootstrapGroup.getId(), + rq.topic(), + rq.partition(), + rq.timestamp(), + rq.key(), + GraalVMConverter.convertInput(rq.input()), + rq.headers(), + rq.maxTimeout() + ) + ) + .map(WebApplicationMocksGraalVMKafka::getPublishMessageRs) + .orElse(null); + } + + @HostAccess.Export + public ConsumeMessagesRs getMessages(@Nonnull Object arg) { + val rq = objectMapper.convertValue(arg, ConsumeMessagesRq.class); + val bootstrapServers = getBootstrapServers(rq); + val messages = getConsumerRecords(rq, bootstrapServers); + + val consumed = messages.stream() + .map(it -> ConsumedMessage.builder() + .objectMapper(objectMapper) + .partition(it.partition()) + .offset(it.offset()) + .timestamp(it.timestamp()) + .timestampType(it.timestampType().name) + .serializedKeySize(it.serializedKeySize()) + .serializedValueSize(it.serializedValueSize()) + .headers(buildHeadersMap(it.headers())) + .key(it.key()) + .value(it.value()) + .build()) + .toList(); + + return ConsumeMessagesRs.builder() + .topic(rq.topic()) + .messages(consumed) + .build(); + } + + private List> getConsumerRecords( + ConsumeMessagesRq rq, + List bootstrapServers + ) { + if (rq.direction() == ConsumeMessagesRq.Direction.EARLIEST) { + return messageConsumerService.getMessages( + bootstrapServers, + rq.topic(), + rq.maxMessages(), + rq.maxTimeout() + ) + .orElseGet(Collections::emptyList); + } + return messageConsumerService.getLastNMessages( + bootstrapServers, + rq.topic(), + rq.maxMessages(), + rq.maxTimeout() + ) + .orElseGet(Collections::emptyList); + + } + + private List getBootstrapServers(ConsumeMessagesRq rq) { + val groupCode = rq.groupCode(); + if (groupCode != null) { + val bootstrapGroup = bootstrapGroupService.getByCode(groupCode); + return bootstrapGroup.getBootstrapServers(); + } + return rq.bootstrapServers(); + } + + private Map buildHeadersMap(Headers headers) { + val headersMap = new HashMap(); + + for (val header : headers) { + headersMap.put(header.key(), header.value()); + } + + return headersMap; + } + +} diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/js/JavaScriptRequestHandler.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/js/JavaScriptRequestHandler.java index f969790..6d5b0da 100644 --- a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/js/JavaScriptRequestHandler.java +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/js/JavaScriptRequestHandler.java @@ -1,10 +1,11 @@ package com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.js; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.sibdevtools.session.api.service.SessionService; import com.github.sibdevtools.storage.api.service.StorageService; import com.github.sibdevtools.web.app.mocks.service.handler.impl.CommonResponsePreparer; import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.GraalVMRequestHandler; +import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.WebApplicationMocksGraalVMSessions; +import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.kafka.WebApplicationMocksGraalVMKafka; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -20,11 +21,12 @@ public class JavaScriptRequestHandler extends GraalVMRequestHandler { @Autowired public JavaScriptRequestHandler(StorageService storageService, - SessionService sessionService, + WebApplicationMocksGraalVMSessions graalVMSessions, + WebApplicationMocksGraalVMKafka graalVMKafka, @Qualifier("webAppMocksObjectMapper") ObjectMapper objectMapper, CommonResponsePreparer commonResponsePreparer) { - super("js", storageService, sessionService, objectMapper, commonResponsePreparer); + super("js", storageService, graalVMSessions, graalVMKafka, objectMapper, commonResponsePreparer); } @Override diff --git a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/python/PythonRequestHandler.java b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/python/PythonRequestHandler.java index 64883a2..de8b06c 100644 --- a/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/python/PythonRequestHandler.java +++ b/web-app/src/main/java/com/github/sibdevtools/web/app/mocks/service/handler/impl/graalvm/python/PythonRequestHandler.java @@ -1,10 +1,11 @@ package com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.python; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.sibdevtools.session.api.service.SessionService; import com.github.sibdevtools.storage.api.service.StorageService; import com.github.sibdevtools.web.app.mocks.service.handler.impl.CommonResponsePreparer; import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.GraalVMRequestHandler; +import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.WebApplicationMocksGraalVMSessions; +import com.github.sibdevtools.web.app.mocks.service.handler.impl.graalvm.dto.kafka.WebApplicationMocksGraalVMKafka; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -20,11 +21,12 @@ public class PythonRequestHandler extends GraalVMRequestHandler { @Autowired public PythonRequestHandler(StorageService storageService, - SessionService sessionService, + WebApplicationMocksGraalVMSessions graalVMSessions, + WebApplicationMocksGraalVMKafka graalVMKafka, @Qualifier("webAppMocksObjectMapper") ObjectMapper objectMapper, CommonResponsePreparer commonResponsePreparer) { - super("python", storageService, sessionService, objectMapper, commonResponsePreparer); + super("python", storageService, graalVMSessions, graalVMKafka, objectMapper, commonResponsePreparer); } @Override