From ec8c7d59e8e1e9d5c988ccb6826af0102261e2e3 Mon Sep 17 00:00:00 2001 From: Olga Davydova Date: Tue, 18 Nov 2025 12:17:30 +0400 Subject: [PATCH 1/3] Added 'X-JSON-BigInt-Encoding' header to Stomp and http --- web/frontend/package.json | 5 ++- .../interceptors/interceptors.module.ts | 2 + .../interceptors/js-header.interceptor.ts | 40 +++++++++++++++++++ .../src/app/core/services/ws.service.ts | 4 ++ .../modal-send-message.component.ts | 4 +- .../shared/live-grid/live-grid.component.ts | 3 +- 6 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 web/frontend/src/app/core/services/interceptors/js-header.interceptor.ts diff --git a/web/frontend/package.json b/web/frontend/package.json index ec9130fd..800d69d9 100644 --- a/web/frontend/package.json +++ b/web/frontend/package.json @@ -52,6 +52,7 @@ "fast-deep-equal": "^3.1.3", "global": "^4.4.0", "gzip-js": "^0.3.2", + "json-bigint": "^1.0.0", "jszip": "^3.10.1", "luxon": "^1.25.0", "lz-string": "^1.4.4", @@ -81,10 +82,10 @@ "@deltix/hd.components-worker": "file:./libs/@deltix/hd.components-worker", "@deltix/hd.components-di": "file:./libs/@deltix/hd.components-di", "@deltix/ng-autocomplete": "file:./libs/@deltix/ng-autocomplete", - "@deltix/hd-date": "file:./libs/@deltix/hd-date", + "@deltix/hd-date": "file:./libs/@deltix/hd-date", "@deltix/ngx-vizceral": "file:./libs/@deltix/ngx-vizceral", "@deltix/vizceral": "file:./libs/@deltix/vizceral", - "@deltix/sso-auth": "file:./libs/@deltix/sso-auth" + "@deltix/sso-auth": "file:./libs/@deltix/sso-auth" }, "peerDependencies": { "postcss": "^8.0.0" diff --git a/web/frontend/src/app/core/services/interceptors/interceptors.module.ts b/web/frontend/src/app/core/services/interceptors/interceptors.module.ts index a91ee77c..ab8c5f1d 100644 --- a/web/frontend/src/app/core/services/interceptors/interceptors.module.ts +++ b/web/frontend/src/app/core/services/interceptors/interceptors.module.ts @@ -4,6 +4,7 @@ import {ApiPrefixesInterceptor} from './api-prefixes.interceptor'; import {AttachTokenInterceptor} from './attach-token.interceptor'; import {CatchConnectionErrorInterceptor} from './catch-connection-error.interceptor'; import {RequestDefaultErrorInterceptor} from './request-default-error.interceptor'; +import { AcceptBigIntFormatInterceptor } from './js-header.interceptor'; @NgModule({ providers: [ @@ -11,6 +12,7 @@ import {RequestDefaultErrorInterceptor} from './request-default-error.intercepto {provide: HTTP_INTERCEPTORS, useClass: AttachTokenInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: ApiPrefixesInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: CatchConnectionErrorInterceptor, multi: true}, + {provide: HTTP_INTERCEPTORS, useClass: AcceptBigIntFormatInterceptor, multi: true}, ], }) export class InterceptorsModule {} diff --git a/web/frontend/src/app/core/services/interceptors/js-header.interceptor.ts b/web/frontend/src/app/core/services/interceptors/js-header.interceptor.ts new file mode 100644 index 00000000..d325f8ae --- /dev/null +++ b/web/frontend/src/app/core/services/interceptors/js-header.interceptor.ts @@ -0,0 +1,40 @@ +import { + HttpEvent, + HttpHandler, + HttpHeaders, + HttpInterceptor, + HttpRequest, +} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; + +@Injectable() +export class AcceptBigIntFormatInterceptor implements HttpInterceptor { + constructor() {} + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + if (req.url.includes('select') || req.url.includes('query') || req.url.includes('filter')) { + const reqUrl = ((req.url[0] !== '/' && req.url[0] !== '.') || + (req.url[0] === '.' && req.url[1] !== '/')) && + req.url.search('http') < 0 + ? '/' + req.url + : req.url; + + const headers = {}; + + req.headers.keys().forEach((key) => { + headers[key] = req.headers.get(key); + }); + + headers['X-JSON-BigInt-Encoding'] = 'string'; + + return next.handle( + req.clone({ + url: reqUrl, + headers: new HttpHeaders(headers), + }), + ); + } + return next.handle(req.clone()); + } +} diff --git a/web/frontend/src/app/core/services/ws.service.ts b/web/frontend/src/app/core/services/ws.service.ts index 82c5c6a0..b4744589 100644 --- a/web/frontend/src/app/core/services/ws.service.ts +++ b/web/frontend/src/app/core/services/ws.service.ts @@ -42,6 +42,10 @@ export class WSService extends RxStomp implements OnDestroy { headers.ack = 'auto'; } + if (!headers['X-JSON-BigInt-Encoding'] && destination.includes('monitor')) { + headers['X-JSON-BigInt-Encoding'] = 'string'; + } + if (!unsubscribeHeaders) { unsubscribeHeaders = { destination: destination, diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-send-message/modal-send-message.component.ts b/web/frontend/src/app/pages/streams/components/modals/modal-send-message/modal-send-message.component.ts index 4d772e3e..d98a0a13 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-send-message/modal-send-message.component.ts +++ b/web/frontend/src/app/pages/streams/components/modals/modal-send-message/modal-send-message.component.ts @@ -29,6 +29,8 @@ import {FieldModel} from '../../../../../shared/utils/d import * as NotificationsActions from '../../../../../core/modules/notifications/store/notifications.actions'; import { getAppSettings } from 'src/app/core/store/app/app.selectors'; +const JSONbig = require('json-bigint')({ storeAsString: true }); + export interface editedMessageProps { symbols?: string[], types?: string[], @@ -452,7 +454,7 @@ export class ModalSendMessageComponent implements OnInit, AfterViewInit, OnDestr } saveJson(field: FieldModel) { - this.formGroup.get(field.name).patchValue(JSON.parse(this.jsonFieldControl.value)); + this.formGroup.get(field.name).patchValue(JSONbig.parse(this.jsonFieldControl.value)); this.jsonFieldControl.setValidators(null); this.jsonFieldControl.patchValue(null); this.editJsonField$.next(null); diff --git a/web/frontend/src/app/shared/live-grid/live-grid.component.ts b/web/frontend/src/app/shared/live-grid/live-grid.component.ts index 8e70a276..4a74118c 100644 --- a/web/frontend/src/app/shared/live-grid/live-grid.component.ts +++ b/web/frontend/src/app/shared/live-grid/live-grid.component.ts @@ -15,7 +15,7 @@ import { } from 'ag-grid-community'; import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; import { Observable, ReplaySubject, Subject, Subscription, of } from 'rxjs'; -import { filter, map, take, takeUntil, withLatestFrom, switchMap } from 'rxjs/operators'; +import { filter, map, take, takeUntil, withLatestFrom, switchMap, distinctUntilChanged } from 'rxjs/operators'; import { TabModel } from 'src/app/pages/streams/models/tab.model'; import { getActiveTab } from 'src/app/pages/streams/store/streams-tabs/streams-tabs.selectors'; import { WebsocketService } from '../../core/services/websocket.service'; @@ -188,6 +188,7 @@ export class LiveGridComponent implements OnInit, OnDestroy, OnChanges { this.appStore.pipe( select(getActiveTab), + distinctUntilChanged((t1, t2) => t1?.stream === t2?.stream), switchMap((tab: TabModel) => { if (tab?.stream?.endsWith('#topic#')) { return this.topicService.getTopicSchema(tab.stream.slice(0, tab.stream.length - 7)); From a2f488ad91e112960ebc504def71f7106f54086f Mon Sep 17 00:00:00 2001 From: Ivan Novik Date: Tue, 18 Nov 2025 11:52:16 +0300 Subject: [PATCH 2/3] add json printer adaptor --- .../tbwg/webapp/config/WebMvcConfig.java | 12 +++ .../webapp/controllers/MonitorController.java | 4 +- .../controllers/MonitorQqlController.java | 6 +- .../controllers/TimebaseController.java | 71 ++++++++++++----- .../webapp/controllers/TopicController.java | 5 +- .../QqlConversionTransformation.java | 4 +- .../services/timebase/MonitorService.java | 6 +- .../services/timebase/MonitorServiceImpl.java | 20 ++--- .../services/timebase/SelectServiceImpl.java | 10 ++- .../services/timebase/base/SelectService.java | 6 +- .../tbwg/webapp/settings/LocaleSettings.java | 35 +++++++++ .../webapp/utils/HeaderAccessorHelper.java | 15 ++++ .../utils/MessageSource2ResponseStream.java | 14 ++-- .../CustomEncodingJsonRawMessagePrinter.java | 76 +++++++++++++++++++ .../webapp/utils/json/JsonBigIntEncoding.java | 6 ++ .../JsonBigIntEncodingArgumentResolver.java | 38 ++++++++++ ...ebGatewayJsonRawMessagePrinterFactory.java | 22 ++++++ .../tbwg/webapp/websockets/WSHandler.java | 9 +-- 18 files changed, 301 insertions(+), 58 deletions(-) create mode 100644 java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/LocaleSettings.java create mode 100644 java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/CustomEncodingJsonRawMessagePrinter.java create mode 100644 java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncoding.java create mode 100644 java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncodingArgumentResolver.java create mode 100644 java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/WebGatewayJsonRawMessagePrinterFactory.java diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebMvcConfig.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebMvcConfig.java index 2642732e..18f1105e 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebMvcConfig.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebMvcConfig.java @@ -18,15 +18,19 @@ import com.epam.deltix.tbwg.webapp.interceptors.TimebaseLoginInterceptor; import com.epam.deltix.tbwg.webapp.interceptors.RestLogInterceptor; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncodingArgumentResolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.util.UrlPathHelper; +import java.util.List; + @Configuration public class WebMvcConfig implements WebMvcConfigurer { @@ -37,6 +41,9 @@ public class WebMvcConfig implements WebMvcConfigurer { private final RestLogInterceptor logInterceptor; private final TimebaseLoginInterceptor timebaseLoginInterceptor; + @Autowired + private JsonBigIntEncodingArgumentResolver argumentResolver; + @Autowired public WebMvcConfig(AsyncTaskExecutor asyncTaskExecutor, RestLogInterceptor logInterceptor, @@ -65,4 +72,9 @@ public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(logInterceptor); registry.addInterceptor(timebaseLoginInterceptor); } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(argumentResolver); + } } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorController.java index 159ba682..dfe4c7e1 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorController.java @@ -19,6 +19,7 @@ import com.epam.deltix.tbwg.webapp.config.WebSocketConfig; import com.epam.deltix.tbwg.webapp.services.timebase.MonitorService; import com.epam.deltix.tbwg.webapp.utils.HeaderAccessorHelper; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; import com.epam.deltix.tbwg.webapp.websockets.subscription.Subscription; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionChannel; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionController; @@ -53,8 +54,9 @@ public Subscription onSubscribe(SimpMessageHeaderAccessor headerAccessor, Subscr long fromTimestamp = headerAccessorHelper.getTimestamp(headerAccessor); List symbols = headerAccessorHelper.getSymbols(headerAccessor); List types = headerAccessorHelper.getTypes(headerAccessor); + JsonBigIntEncoding bigIntEncoding = HeaderAccessorHelper.getJsonBigIntEncoding(headerAccessor); - monitorService.subscribe(sessionId, subscriptionId, stream, null, fromTimestamp, types, symbols, channel::sendMessage); + monitorService.subscribe(sessionId, subscriptionId, stream, null, fromTimestamp, types, symbols, channel::sendMessage, bigIntEncoding); return () -> monitorService.unsubscribe(sessionId, subscriptionId); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorQqlController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorQqlController.java index 471dee27..b60ef833 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorQqlController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorQqlController.java @@ -16,11 +16,10 @@ */ package com.epam.deltix.tbwg.webapp.controllers; -import com.epam.deltix.gflog.api.Log; -import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.tbwg.webapp.config.WebSocketConfig; import com.epam.deltix.tbwg.webapp.services.timebase.MonitorService; import com.epam.deltix.tbwg.webapp.utils.HeaderAccessorHelper; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; import com.epam.deltix.tbwg.webapp.websockets.subscription.Subscription; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionChannel; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionController; @@ -55,8 +54,9 @@ public Subscription onSubscribe(SimpMessageHeaderAccessor headerAccessor, Subscr long fromTimestamp = headerAccessorHelper.getTimestamp(headerAccessor); List symbols = headerAccessorHelper.getSymbols(headerAccessor); List types = headerAccessorHelper.getTypes(headerAccessor); + JsonBigIntEncoding bigIntEncoding = HeaderAccessorHelper.getJsonBigIntEncoding(headerAccessor); - monitorService.subscribe(sessionId, subscriptionId, null, qql, fromTimestamp, types, symbols, channel::sendMessage); + monitorService.subscribe(sessionId, subscriptionId, null, qql, fromTimestamp, types, symbols, channel::sendMessage, bigIntEncoding); return () -> monitorService.unsubscribe(sessionId, subscriptionId); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TimebaseController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TimebaseController.java index 318390c9..21c6547e 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TimebaseController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TimebaseController.java @@ -25,6 +25,7 @@ import com.epam.deltix.qsrv.hf.tickdb.pub.lock.LockType; import com.epam.deltix.qsrv.hf.tickdb.ui.tbshell.TickDBShell; import com.epam.deltix.tbwg.webapp.model.smd.CurrencyDef; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; import com.epam.deltix.timebase.messages.IdentityKey; import com.epam.deltix.timebase.messages.InstrumentKey; import com.epam.deltix.timebase.messages.InstrumentMessage; @@ -175,14 +176,14 @@ public long correlationId() { */ @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/select", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity select(@Valid @RequestBody(required = false) SelectRequest select) - throws NoStreamsException { + public ResponseEntity select(@Valid @RequestBody(required = false) SelectRequest select, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { if (select == null) { select = new SelectRequest(); } return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) - .body(selectService.select(select, MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET)); + .body(selectService.select(select, MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET, bigIntEncoding)); } /** @@ -217,7 +218,8 @@ public ResponseEntity select( @RequestParam(required = false) Long offset, @RequestParam(required = false) Integer rows, @RequestParam(required = false) String space, - @RequestParam(required = false) boolean reverse) throws NoStreamsException { + @RequestParam(required = false) boolean reverse, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { SelectRequest request = new SelectRequest(); request.streams = streams; request.symbols = symbols; @@ -230,7 +232,7 @@ public ResponseEntity select( request.reverse = reverse; request.depth = depth; request.space = space; - return select(request); + return select(request, bigIntEncoding); } /** @@ -249,13 +251,14 @@ public ResponseEntity select( @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/select", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity select(@PathVariable String streamId, - @Valid @RequestBody(required = false) StreamRequest select) + @Valid @RequestBody(required = false) StreamRequest select, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { if (select == null) select = new StreamRequest(); return select(streamId, select.symbols, select.types, null, select.from, select.to, select.offset, - select.rows, select.space, select.reverse); + select.rows, select.space, select.reverse, bigIntEncoding); } /** @@ -297,11 +300,12 @@ public ResponseEntity select( @RequestParam(required = false) Long offset, @RequestParam(required = false) Integer rows, @RequestParam(required = false) String space, - @RequestParam(required = false) boolean reverse) throws NoStreamsException { + @RequestParam(required = false) boolean reverse, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { if (TextUtils.isEmpty(streamId)) throw new NoStreamsException(); - return select(new String[]{streamId}, symbols, types, depth, from, to, offset, rows, space, reverse); + return select(new String[]{streamId}, symbols, types, depth, from, to, offset, rows, space, reverse, bigIntEncoding); } // download operation is permitted for any user @@ -1025,7 +1029,7 @@ ResponseEntity checkWritable(String error) { @RequestMapping(value = "/{streamId}/{symbolId}/select", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity select(@PathVariable String streamId, @PathVariable String symbolId, @Valid @RequestBody(required = false) InstrumentRequest select, - OutputStream outputStream) { + OutputStream outputStream, JsonBigIntEncoding bigIntEncoding) { DXTickStream stream = service.getStream(streamId); if (stream == null) @@ -1054,7 +1058,7 @@ public ResponseEntity select(@PathVariable String streamI .contentType(MediaType.APPLICATION_JSON) .body(new MessageSource2ResponseStream( stream.select(startTime, options, select.types, ids), select.getEndTime(), startIndex, endIndex, - MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET) + MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET, bigIntEncoding) ); } @@ -1090,14 +1094,15 @@ public ResponseEntity select( @RequestParam(required = false) Long offset, @RequestParam(required = false) Integer rows, @RequestParam(required = false) String space, - @RequestParam(required = false) boolean reverse) throws NoStreamsException { + @RequestParam(required = false) boolean reverse, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { if (TextUtils.isEmpty(streamId)) throw new NoStreamsException(); if (TextUtils.isEmpty(symbolId)) return ResponseEntity.notFound().build(); - return select(new String[]{streamId}, new String[]{symbolId}, types, depth, from, to, offset, rows, space, reverse); + return select(new String[]{streamId}, new String[]{symbolId}, types, depth, from, to, offset, rows, space, reverse, bigIntEncoding); } private SelectionOptions getSelectionOption(BaseRequest r) { @@ -1535,9 +1540,8 @@ public ResponseEntity streams(@RequestParam(required = false, defau */ @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/query", method = {RequestMethod.POST}) - public ResponseEntity query(Principal principal, @Valid @RequestBody(required = false) QueryRequest select) - throws InvalidQueryException, WriteOperationsException { - + public ResponseEntity query(Principal principal, @Valid @RequestBody(required = false) QueryRequest select, + JsonBigIntEncoding bigIntEncoding) throws InvalidQueryException, WriteOperationsException { if (select == null || StringUtils.isEmpty(select.query)) throw new InvalidQueryException(select == null ? "" : select.query); @@ -1559,7 +1563,34 @@ public ResponseEntity query(Principal principal, @Valid @ .body(new MessageSource2ResponseStream( service.getConnection().executeQuery( select.query, options, null, null, select.getStartTime(Long.MIN_VALUE), select.getEndTime(Long.MIN_VALUE)), - select.getEndTime(), startIndex, endIndex, MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET)); + select.getEndTime(), startIndex, endIndex, MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET, bigIntEncoding)); + } + + /** + * Executes an QQL query and returns the maximum possible number of records + */ + @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") + @RequestMapping(value = "/unlimitedQuery", method = {RequestMethod.POST}) + public ResponseEntity unlimitedQuery(Principal principal, @Valid @RequestBody(required = false) QueryRequest select, + JsonBigIntEncoding bigIntEncoding) + throws InvalidQueryException, WriteOperationsException { + + if (select == null || StringUtils.isEmpty(select.query)) + throw new InvalidQueryException(select == null ? "" : select.query); + if (service.isReadonly() && (select.query.toLowerCase().contains("drop") || select.query.toLowerCase().contains("create"))) + throw new WriteOperationsException("CREATE or DROP"); + if (isDdlQuery(select.query) && !hasAuthority(principal, "TB_ALLOW_WRITE")) { + throw new AccessDeniedException("TB_ALLOW_WRITE permission required."); + } + + SelectionOptions options = getSelectionOption(select); + LOGGER.info().append("UNLIMITED QUERY: (").append(select.query).append(")").commit(); + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(new MessageSource2ResponseStream( + service.getConnection().executeQuery( + select.query, options, null, null, select.getStartTime(Long.MIN_VALUE), select.getEndTime(Long.MIN_VALUE)), + select.getEndTime(), 0, Integer.MAX_VALUE, Integer.MAX_VALUE, bigIntEncoding)); } private boolean isDdlQuery(String query) { @@ -1663,8 +1694,8 @@ public Set queryFunctionsShort() { @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/filter", method = {RequestMethod.POST}, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity filter(@PathVariable String streamId, @Valid @RequestBody FilterRequest filter) - throws UnknownStreamException { + public ResponseEntity filter(@PathVariable String streamId, @Valid @RequestBody FilterRequest filter, + JsonBigIntEncoding bigIntEncoding) throws UnknownStreamException { DXTickStream stream = service.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -1697,7 +1728,7 @@ public ResponseEntity filter(@PathVariable String streamI .contentType(MediaType.APPLICATION_JSON) .body(new MessageSource2ResponseStream(service.getConnection() .executeQuery(query, options, null, null, startTime), endTime, startIndex, endIndex, - MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET)); + MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET, bigIntEncoding)); } /** diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TopicController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TopicController.java index 59658e5a..240c89da 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TopicController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TopicController.java @@ -28,6 +28,8 @@ import com.epam.deltix.tbwg.webapp.model.tree.TreeNodeDef; import com.epam.deltix.tbwg.webapp.services.timebase.MonitorService; import com.epam.deltix.tbwg.webapp.services.topic.TopicService; +import com.epam.deltix.tbwg.webapp.utils.HeaderAccessorHelper; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; import com.epam.deltix.tbwg.webapp.websockets.subscription.Subscription; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionChannel; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionController; @@ -126,8 +128,9 @@ public Subscription onSubscribe(SimpMessageHeaderAccessor header, SubscriptionCh String topicKey = URLDecoder.decode(extractId(destination), StandardCharsets.UTF_8); String sessionId = header.getSessionId(); String subscriptionId = header.getSubscriptionId(); + JsonBigIntEncoding bigIntEncoding = HeaderAccessorHelper.getJsonBigIntEncoding(header); - monitorService.subscribeTopic(sessionId, subscriptionId, topicKey, channel::sendMessage); + monitorService.subscribeTopic(sessionId, subscriptionId, topicKey, channel::sendMessage, bigIntEncoding); return () -> monitorService.unsubscribe(sessionId, subscriptionId); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/transformations/QqlConversionTransformation.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/transformations/QqlConversionTransformation.java index e7106c4c..568dab29 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/transformations/QqlConversionTransformation.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/transformations/QqlConversionTransformation.java @@ -20,13 +20,15 @@ import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; import com.epam.deltix.tbwg.messages.Message; import com.epam.deltix.tbwg.webapp.model.charting.line.RawElementDef; +import com.epam.deltix.tbwg.webapp.utils.json.WebGatewayJsonRawMessagePrinterFactory; import java.util.Collections; +@Deprecated // not implemented on the front-end side public class QqlConversionTransformation extends AbstractChartTransformation { private final StringBuilder sb = new StringBuilder(); - private final JSONRawMessagePrinter rawMessagePrinter = new JSONRawMessagePrinter(); + private final JSONRawMessagePrinter rawMessagePrinter = WebGatewayJsonRawMessagePrinterFactory.create(); public QqlConversionTransformation() { super(Collections.singletonList(RawMessage.class), Collections.singletonList(RawElementDef.class)); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorService.java index 7c4188b6..e0a0d225 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorService.java @@ -16,16 +16,18 @@ */ package com.epam.deltix.tbwg.webapp.services.timebase; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; + import java.util.List; import java.util.function.Consumer; public interface MonitorService { void subscribe(String sessionId, String subscriptionId, String key, String qql, long fromTimestamp, List types, - List symbols, Consumer consumer); + List symbols, Consumer consumer, JsonBigIntEncoding bigIntEncoding); void subscribeTopic(String sessionId, String subscriptionId, String key, - Consumer consumer); + Consumer consumer, JsonBigIntEncoding bigIntEncoding); void unsubscribe(String sessionId, String subscriptionId); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorServiceImpl.java index ede2acc5..1f32feb4 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorServiceImpl.java @@ -16,13 +16,12 @@ */ package com.epam.deltix.tbwg.webapp.services.timebase; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; +import com.epam.deltix.tbwg.webapp.utils.json.WebGatewayJsonRawMessagePrinterFactory; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import com.epam.deltix.qsrv.hf.pub.RawMessage; -import com.epam.deltix.qsrv.util.json.DataEncoding; import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; -import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinterFactory; -import com.epam.deltix.qsrv.util.json.PrintType; import com.epam.deltix.tbwg.webapp.utils.TBWGUtils; import com.epam.deltix.tbwg.webapp.utils.cache.CachedMessageBufferImpl; import org.springframework.stereotype.Service; @@ -53,9 +52,9 @@ public MonitorServiceImpl(TimebaseService timebase) { @Override public synchronized void subscribe(String sessionId, String subscriptionId, String key, String qql, long fromTimestamp, List types, - List symbols, Consumer consumer) + List symbols, Consumer consumer, JsonBigIntEncoding bigIntEncoding) { - BufferedConsumer bufferedConsumer = new BufferedConsumer(); + BufferedConsumer bufferedConsumer = new BufferedConsumer(WebGatewayJsonRawMessagePrinterFactory.create(bigIntEncoding)); StreamConsumer streamConsumer = new StreamConsumer(timebase, fromTimestamp, key, qql, symbols, types, bufferedConsumer); ScheduledFuture scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(() -> { String messages = bufferedConsumer.messageBuffer.flush(); @@ -70,9 +69,9 @@ public synchronized void subscribe(String sessionId, String subscriptionId, Stri @Override public synchronized void subscribeTopic(String sessionId, String subscriptionId, String key, - Consumer consumer) + Consumer consumer, JsonBigIntEncoding bigIntEncoding) { - BufferedConsumer bufferedConsumer = new BufferedConsumer(); + BufferedConsumer bufferedConsumer = new BufferedConsumer(WebGatewayJsonRawMessagePrinterFactory.create(bigIntEncoding)); TopicConsumer topicConsumer = new TopicConsumer(timebase, key, bufferedConsumer); ScheduledFuture scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(() -> { String messages = bufferedConsumer.messageBuffer.flush(); @@ -102,10 +101,11 @@ public synchronized void preDestroy() { private static final class BufferedConsumer implements Consumer { - private final JSONRawMessagePrinter printer = - new JSONRawMessagePrinter(false, true,DataEncoding.STANDARD, true, true,PrintType.FULL, "$type"); + private final CachedMessageBufferImpl messageBuffer; - private final CachedMessageBufferImpl messageBuffer = new CachedMessageBufferImpl(printer); + public BufferedConsumer(JSONRawMessagePrinter printer) { + messageBuffer = new CachedMessageBufferImpl(printer); + } @Override public void accept(RawMessage rawMessage) { diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SelectServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SelectServiceImpl.java index a2a4a447..67f7070c 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SelectServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SelectServiceImpl.java @@ -20,6 +20,7 @@ import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.qsrv.hf.pub.ChannelQualityOfService; import com.epam.deltix.tbwg.webapp.services.timebase.base.SelectService; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; import com.epam.deltix.timebase.messages.IdentityKey; import com.epam.deltix.qsrv.hf.tickdb.pub.DXTickStream; import com.epam.deltix.qsrv.hf.tickdb.pub.SelectionOptions; @@ -52,7 +53,8 @@ public SelectServiceImpl(TimebaseService timebase) { @Override public MessageSource2ResponseStream select(long startTime, long endTime, long offset, int rows, boolean reverse, - String[] types, String[] symbols, String[] keys, String space, int maxRecords) + String[] types, String[] symbols, String[] keys, String space, int maxRecords, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { List streams = getStreams(keys); @@ -81,15 +83,15 @@ public MessageSource2ResponseStream select(long startTime, long endTime, long of .append("AND timestamp [").append(GMT.formatDateTimeMillis(startTime)).append(":") .append(GMT.formatDateTimeMillis(endTime)).append("]") .commit(); - return new MessageSource2ResponseStream(source, endTime, startIndex, endIndex, maxRecords); + return new MessageSource2ResponseStream(source, endTime, startIndex, endIndex, maxRecords, bigIntEncoding); } @Override - public MessageSource2ResponseStream select(SelectRequest selectRequest, int maxRecords) throws NoStreamsException { + public MessageSource2ResponseStream select(SelectRequest selectRequest, int maxRecords, JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { List streams = getStreams(selectRequest.streams); long startTime = selectRequest.getStartTime(getEndTime(streams)); return select(startTime, selectRequest.getEndTime(), selectRequest.offset, selectRequest.rows, selectRequest.reverse, - selectRequest.types, selectRequest.symbols, selectRequest.streams, selectRequest.space, maxRecords); + selectRequest.types, selectRequest.symbols, selectRequest.streams, selectRequest.space, maxRecords, bigIntEncoding); } private List getStreams(String ... streamKeys) throws NoStreamsException { diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/base/SelectService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/base/SelectService.java index 2d25a54c..1970cab7 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/base/SelectService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/base/SelectService.java @@ -19,13 +19,15 @@ import com.epam.deltix.tbwg.webapp.model.input.SelectRequest; import com.epam.deltix.tbwg.webapp.services.timebase.exc.NoStreamsException; import com.epam.deltix.tbwg.webapp.utils.MessageSource2ResponseStream; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; public interface SelectService { MessageSource2ResponseStream select(long startTime, long endTime, long offset, int rows, boolean reverse, - String[] types, String[] symbols, String[] keys, String space, int maxRecords) + String[] types, String[] symbols, String[] keys, String space, int maxRecords, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException; - MessageSource2ResponseStream select(SelectRequest selectRequest, int maxRecords) throws NoStreamsException; + MessageSource2ResponseStream select(SelectRequest selectRequest, int maxRecords, JsonBigIntEncoding bigIntEncoding) throws NoStreamsException; } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/LocaleSettings.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/LocaleSettings.java new file mode 100644 index 00000000..c2a118c4 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/LocaleSettings.java @@ -0,0 +1,35 @@ +package com.epam.deltix.tbwg.webapp.settings; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +public class LocaleSettings { + + @Value("${spring.web.locale:}") + private String locale; + + private static Locale applicationLocale; + + public static Locale getApplicationLocale() { + return applicationLocale; + } + + @PostConstruct + private void setup() { + Set available = Arrays.stream(Locale.getAvailableLocales()).map(Locale::toLanguageTag).collect(Collectors.toSet()); + if (available.contains(locale)) { + applicationLocale = Locale.forLanguageTag(locale); + } else { + applicationLocale = Locale.getDefault(); + } + } + + +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/HeaderAccessorHelper.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/HeaderAccessorHelper.java index cf48bf0d..cb47624e 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/HeaderAccessorHelper.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/HeaderAccessorHelper.java @@ -16,6 +16,8 @@ */ package com.epam.deltix.tbwg.webapp.utils; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncodingArgumentResolver; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.epam.deltix.gflog.api.Log; @@ -47,6 +49,19 @@ public long getTimestamp(SimpMessageHeaderAccessor headerAccessor) { return Long.MIN_VALUE; } + public static JsonBigIntEncoding getJsonBigIntEncoding(SimpMessageHeaderAccessor accessor) { + String headerValue = accessor.getFirstNativeHeader(JsonBigIntEncodingArgumentResolver.BIG_INT_ENCODING_HEADER); + if (headerValue != null) { + try { + return JsonBigIntEncoding.valueOf(headerValue.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + LOG.warn("Unknown value for %s: '%s', using default: %s") + .with(JsonBigIntEncodingArgumentResolver.BIG_INT_ENCODING_HEADER).with(headerValue).with(JsonBigIntEncodingArgumentResolver.DEFAULT_ENCODING); + } + } + return JsonBigIntEncodingArgumentResolver.DEFAULT_ENCODING; + } + public List getSymbols(SimpMessageHeaderAccessor headerAccessor) { List headers = headerAccessor.getNativeHeader(SYMBOLS_HEADER); if (headers == null || headers.isEmpty()) { diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/MessageSource2ResponseStream.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/MessageSource2ResponseStream.java index 7f8f2725..34fdb760 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/MessageSource2ResponseStream.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/MessageSource2ResponseStream.java @@ -20,9 +20,9 @@ import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.qsrv.hf.pub.RawMessage; import com.epam.deltix.qsrv.hf.tickdb.pub.query.InstrumentMessageSource; -import com.epam.deltix.qsrv.util.json.DataEncoding; import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; -import com.epam.deltix.qsrv.util.json.PrintType; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; +import com.epam.deltix.tbwg.webapp.utils.json.WebGatewayJsonRawMessagePrinterFactory; import com.epam.deltix.util.lang.Util; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; @@ -41,28 +41,28 @@ public class MessageSource2ResponseStream implements StreamingResponseBody { private final long endIndex; // inclusive private final int maxRecords; - private final JSONRawMessagePrinter printer = - new JSONRawMessagePrinter(false, true, DataEncoding.STANDARD, true, - false, PrintType.FULL, true, "$type"); + private final JSONRawMessagePrinter printer; private final StringBuilder sb = new StringBuilder(); @SuppressWarnings({"unused"}) - public MessageSource2ResponseStream(InstrumentMessageSource source, int maxRecords) { + public MessageSource2ResponseStream(InstrumentMessageSource source, int maxRecords, JsonBigIntEncoding bigIntEncoding) { this.source = source; this.toTimestamp = Long.MAX_VALUE; this.startIndex = 0; this.endIndex = Integer.MAX_VALUE; this.maxRecords = maxRecords; + this.printer = WebGatewayJsonRawMessagePrinterFactory.create(bigIntEncoding); } public MessageSource2ResponseStream(InstrumentMessageSource messageSource, long toTimestamp, long startIndex, - long endIndex, int maxRecords) { + long endIndex, int maxRecords, JsonBigIntEncoding bigIntEncoding) { this.source = messageSource; this.toTimestamp = toTimestamp; this.startIndex = startIndex; this.endIndex = endIndex; this.maxRecords = maxRecords; + this.printer = WebGatewayJsonRawMessagePrinterFactory.create(bigIntEncoding); } @Override diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/CustomEncodingJsonRawMessagePrinter.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/CustomEncodingJsonRawMessagePrinter.java new file mode 100644 index 00000000..c3f04fd7 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/CustomEncodingJsonRawMessagePrinter.java @@ -0,0 +1,76 @@ +package com.epam.deltix.tbwg.webapp.utils.json; + +import com.epam.deltix.qsrv.hf.pub.NullValueException; +import com.epam.deltix.qsrv.hf.pub.ReadableValue; +import com.epam.deltix.qsrv.hf.pub.codec.NonStaticFieldInfo; +import com.epam.deltix.qsrv.hf.pub.md.ArrayDataType; +import com.epam.deltix.qsrv.hf.pub.md.DataType; +import com.epam.deltix.qsrv.hf.pub.md.IntegerDataType; +import com.epam.deltix.qsrv.util.json.DataEncoding; +import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; +import com.epam.deltix.qsrv.util.json.PrintType; + + +import java.util.Locale; + +public class CustomEncodingJsonRawMessagePrinter extends JSONRawMessagePrinter { + + + /** + * This printer only supports STANDARD DataEncoding. + * There are optimizations present in this implementation that may produce incorrect results + * if used with other DataEncoding values. + */ + public CustomEncodingJsonRawMessagePrinter(Locale locale) { + super(false, true, DataEncoding.STANDARD, true, true, PrintType.FULL, false, "$type", locale); + } + + protected boolean appendFieldValue(ReadableValue decoder, NonStaticFieldInfo field, StringBuilder sb) { + DataType dataType = field.getType(); + if (dataType instanceof IntegerDataType && ((IntegerDataType) dataType).getSize() == 8) { + try { + long v = decoder.getLong(); + appendString(String.valueOf(v), sb); + return ((IntegerDataType) dataType).getNullValue() != v; + } catch (NullValueException e) { + return false; + } + } else { + return super.appendFieldValue(decoder, field, sb); + } + } + + protected boolean appendArrayField(ArrayDataType type, ReadableValue udec, StringBuilder sb) throws NullValueException { + final DataType underlineType = type.getElementDataType(); + if (underlineType instanceof IntegerDataType && ((IntegerDataType) underlineType).getSize() == 8) { + final int len = udec.getArrayLength(); + appendBlock('[', sb); + boolean needSepa = false; + for (int i = 0; i < len; i++) { + try { + final ReadableValue rv = udec.nextReadableElement(); + if (needSepa) + appendSeparator(sb); + else + needSepa = true; + sb.append(rv.getString()); + } catch (NullValueException e) { + sb.append("null"); + } + } + appendBlock(']', sb); + return true; + } else { + return super.appendArrayField(type, udec, sb); + } + } + + protected void appendStaticValue(DataType dataType, String value, StringBuilder sb) { + if (dataType instanceof IntegerDataType && ((IntegerDataType) dataType).getSize() == 8) { + sb.append('"').append(value).append('"'); + } else { + super.appendStaticValue(dataType, value, sb); + } + } + +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncoding.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncoding.java new file mode 100644 index 00000000..850aafaa --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncoding.java @@ -0,0 +1,6 @@ +package com.epam.deltix.tbwg.webapp.utils.json; + +public enum JsonBigIntEncoding { + STRING, + NUMBER +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncodingArgumentResolver.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncodingArgumentResolver.java new file mode 100644 index 00000000..def88f57 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncodingArgumentResolver.java @@ -0,0 +1,38 @@ +package com.epam.deltix.tbwg.webapp.utils.json; + +import com.epam.deltix.gflog.api.Log; +import com.epam.deltix.gflog.api.LogFactory; +import org.jetbrains.annotations.NotNull; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class JsonBigIntEncodingArgumentResolver implements HandlerMethodArgumentResolver { + + private static final Log LOG = LogFactory.getLog(JsonBigIntEncodingArgumentResolver.class); + public static final String BIG_INT_ENCODING_HEADER = "X-JSON-BigInt-Encoding"; + public static final JsonBigIntEncoding DEFAULT_ENCODING = JsonBigIntEncoding.NUMBER; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType() == JsonBigIntEncoding.class; + } + + @Override + public Object resolveArgument(@NotNull MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, org.springframework.web.bind.support.WebDataBinderFactory binderFactory) { + String headerValue = webRequest.getHeader(BIG_INT_ENCODING_HEADER); + if (headerValue != null) { + try { + return JsonBigIntEncoding.valueOf(headerValue.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + LOG.warn("Unknown value for %s: '%s', using default: %s") + .with(BIG_INT_ENCODING_HEADER).with(headerValue).with(DEFAULT_ENCODING); + } + } + return DEFAULT_ENCODING; + } +} \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/WebGatewayJsonRawMessagePrinterFactory.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/WebGatewayJsonRawMessagePrinterFactory.java new file mode 100644 index 00000000..d5c245e2 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/WebGatewayJsonRawMessagePrinterFactory.java @@ -0,0 +1,22 @@ +package com.epam.deltix.tbwg.webapp.utils.json; + +import com.epam.deltix.qsrv.util.json.DataEncoding; +import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; +import com.epam.deltix.qsrv.util.json.PrintType; +import com.epam.deltix.tbwg.webapp.settings.LocaleSettings; + +import java.util.Locale; + +public class WebGatewayJsonRawMessagePrinterFactory { + + public static JSONRawMessagePrinter create() { + return create(JsonBigIntEncoding.NUMBER); + } + public static JSONRawMessagePrinter create(JsonBigIntEncoding bigIntEncoding) { + Locale locale = LocaleSettings.getApplicationLocale(); + if (bigIntEncoding == JsonBigIntEncoding.STRING) { + return new CustomEncodingJsonRawMessagePrinter(locale); + } + return new JSONRawMessagePrinter(false, true, DataEncoding.STANDARD, true, true, PrintType.FULL, false, "$type", locale); + } +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/WSHandler.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/WSHandler.java index 560e45ac..4ab31983 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/WSHandler.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/WSHandler.java @@ -24,6 +24,7 @@ import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.services.timebase.connections.TbUserDetails; import com.epam.deltix.tbwg.webapp.utils.TBWGUtils; +import com.epam.deltix.tbwg.webapp.utils.json.WebGatewayJsonRawMessagePrinterFactory; import com.epam.deltix.timebase.messages.IdentityKey; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -32,9 +33,7 @@ import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.qsrv.hf.pub.RawMessage; import com.epam.deltix.qsrv.hf.tickdb.pub.topic.DirectChannel; -import com.epam.deltix.qsrv.util.json.DataEncoding; import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; -import com.epam.deltix.qsrv.util.json.PrintType; import com.epam.deltix.tbwg.webapp.utils.cache.CachedMessageBufferImpl; import com.epam.deltix.tbwg.webapp.utils.cache.MessageBuffer; import com.epam.deltix.tbwg.webapp.utils.cache.MessageBufferImpl; @@ -79,11 +78,6 @@ public class WSHandler extends TextWebSocketHandler { protected final class PumpTask extends QuickExecutor.QuickTask { final Runnable avlnr = PumpTask.this::submit; - private final JSONRawMessagePrinter printer - = new JSONRawMessagePrinter(false, true, DataEncoding.STANDARD, true, true, PrintType.FULL, "$type"); - - //final JSONRawMessagePrinter printer = new JSONRawMessagePrinter(false, true); - private final TickCursor cursor; private final DXTickStream[] selection; private final IntermittentlyAvailableCursor c; @@ -104,6 +98,7 @@ public PumpTask (DXTickStream[] selection, TickCursor cursor, long toTimestamp, this.c = (IntermittentlyAvailableCursor)cursor; this.toTimestamp = toTimestamp; this.session = session; + JSONRawMessagePrinter printer = WebGatewayJsonRawMessagePrinterFactory.create(); this.buffer = useCache() ? new CachedMessageBufferImpl(printer) : new MessageBufferImpl(printer, live); sendCounter = metrics.endpointCounter(WebSocketConfig.SEND_MESSAGES_METRIC, endpoint()); From abc26e75b5c6defee374153ac119c8c8338337a5 Mon Sep 17 00:00:00 2001 From: Olga Davydova Date: Tue, 18 Nov 2025 12:55:32 +0400 Subject: [PATCH 3/3] Fixed format function for big numbers --- web/frontend/src/app/shared/services/grid.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/frontend/src/app/shared/services/grid.service.ts b/web/frontend/src/app/shared/services/grid.service.ts index 41b27c36..e85bcf5d 100644 --- a/web/frontend/src/app/shared/services/grid.service.ts +++ b/web/frontend/src/app/shared/services/grid.service.ts @@ -369,7 +369,7 @@ export class GridService implements OnDestroy { return params.value.toFixed(exponent); } - if (['number', 'string'].includes(typeof params.value) && Math.abs(params.value) >= 1000) { + if (typeof params.value === 'number' && Math.abs(params.value) >= 1000 && Math.abs(params.value) <= Number.MAX_SAFE_INTEGER) { return parseFloat(params.value).toLocaleString(this.locale); }