refactor: silence sonar
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
1 import { type FSWatcher, readFileSync, watch } from 'node:fs';
2 import { dirname, join, resolve } from 'node:path';
3 import { fileURLToPath } from 'node:url';
4
5 import chalk from 'chalk';
6 import merge from 'just-merge';
7
8 import { Constants } from './Constants';
9 import {
10 hasOwnProp,
11 isCFEnvironment,
12 isNotEmptyString,
13 isUndefined,
14 logPrefix,
15 once,
16 } from './Utils';
17 import {
18 ApplicationProtocol,
19 type ConfigurationData,
20 ConfigurationSection,
21 FileType,
22 type LogConfiguration,
23 type StationTemplateUrl,
24 type StorageConfiguration,
25 StorageType,
26 SupervisionUrlDistribution,
27 type UIServerConfiguration,
28 type WorkerConfiguration,
29 } from '../types';
30 import {
31 DEFAULT_ELEMENT_START_DELAY,
32 DEFAULT_POOL_MAX_SIZE,
33 DEFAULT_POOL_MIN_SIZE,
34 DEFAULT_WORKER_START_DELAY,
35 WorkerProcessType,
36 } from '../worker';
37
38 type ConfigurationSectionType =
39 | LogConfiguration
40 | StorageConfiguration
41 | WorkerConfiguration
42 | UIServerConfiguration;
43
44 export class Configuration {
45 private static configurationFile = join(
46 dirname(fileURLToPath(import.meta.url)),
47 'assets',
48 'config.json',
49 );
50
51 private static configurationData?: ConfigurationData;
52 private static configurationFileWatcher?: FSWatcher;
53 private static configurationSectionCache = new Map<
54 ConfigurationSection,
55 ConfigurationSectionType
56 >([
57 [ConfigurationSection.log, Configuration.buildLogSection()],
58 [ConfigurationSection.performanceStorage, Configuration.buildPerformanceStorageSection()],
59 [ConfigurationSection.worker, Configuration.buildWorkerSection()],
60 [ConfigurationSection.uiServer, Configuration.buildUIServerSection()],
61 ]);
62
63 private static configurationChangeCallback?: () => Promise<void>;
64
65 private constructor() {
66 // This is intentional
67 }
68
69 public static setConfigurationChangeCallback(cb: () => Promise<void>): void {
70 Configuration.configurationChangeCallback = cb;
71 }
72
73 public static getConfigurationSection<T extends ConfigurationSectionType>(
74 sectionName: ConfigurationSection,
75 ): T {
76 if (!Configuration.isConfigurationSectionCached(sectionName)) {
77 Configuration.cacheConfigurationSection(sectionName);
78 }
79 return Configuration.configurationSectionCache.get(sectionName) as T;
80 }
81
82 public static getStationTemplateUrls(): StationTemplateUrl[] | undefined {
83 const checkDeprecatedConfigurationKeysOnce = once(
84 Configuration.checkDeprecatedConfigurationKeys.bind(Configuration),
85 Configuration,
86 );
87 checkDeprecatedConfigurationKeysOnce();
88 return Configuration.getConfigurationData()?.stationTemplateUrls;
89 }
90
91 public static getSupervisionUrls(): string | string[] | undefined {
92 if (
93 !isUndefined(
94 Configuration.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData],
95 )
96 ) {
97 Configuration.getConfigurationData()!.supervisionUrls = Configuration.getConfigurationData()![
98 'supervisionURLs' as keyof ConfigurationData
99 ] as string | string[];
100 }
101 return Configuration.getConfigurationData()?.supervisionUrls;
102 }
103
104 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution | undefined {
105 return hasOwnProp(Configuration.getConfigurationData(), 'supervisionUrlDistribution')
106 ? Configuration.getConfigurationData()?.supervisionUrlDistribution
107 : SupervisionUrlDistribution.ROUND_ROBIN;
108 }
109
110 public static workerPoolInUse(): boolean {
111 return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
112 Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
113 .processType!,
114 );
115 }
116
117 public static workerDynamicPoolInUse(): boolean {
118 return (
119 Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
120 .processType === WorkerProcessType.dynamicPool
121 );
122 }
123
124 private static logPrefix = (): string => {
125 return logPrefix(' Simulator configuration |');
126 };
127
128 private static isConfigurationSectionCached(sectionName: ConfigurationSection): boolean {
129 return Configuration.configurationSectionCache.has(sectionName);
130 }
131
132 private static cacheConfigurationSection(sectionName: ConfigurationSection): void {
133 switch (sectionName) {
134 case ConfigurationSection.log:
135 Configuration.configurationSectionCache.set(sectionName, Configuration.buildLogSection());
136 break;
137 case ConfigurationSection.performanceStorage:
138 Configuration.configurationSectionCache.set(
139 sectionName,
140 Configuration.buildPerformanceStorageSection(),
141 );
142 break;
143 case ConfigurationSection.worker:
144 Configuration.configurationSectionCache.set(
145 sectionName,
146 Configuration.buildWorkerSection(),
147 );
148 break;
149 case ConfigurationSection.uiServer:
150 Configuration.configurationSectionCache.set(
151 sectionName,
152 Configuration.buildUIServerSection(),
153 );
154 break;
155 default:
156 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
157 throw new Error(`Unknown configuration section '${sectionName}'`);
158 }
159 }
160
161 private static buildUIServerSection(): UIServerConfiguration {
162 let uiServerConfiguration: UIServerConfiguration = {
163 enabled: false,
164 type: ApplicationProtocol.WS,
165 options: {
166 host: Constants.DEFAULT_UI_SERVER_HOST,
167 port: Constants.DEFAULT_UI_SERVER_PORT,
168 },
169 };
170 if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.uiServer)) {
171 uiServerConfiguration = merge<UIServerConfiguration>(
172 uiServerConfiguration,
173 Configuration.getConfigurationData()!.uiServer!,
174 );
175 }
176 if (isCFEnvironment() === true) {
177 delete uiServerConfiguration.options?.host;
178 uiServerConfiguration.options!.port = parseInt(process.env.PORT!);
179 }
180 return uiServerConfiguration;
181 }
182
183 private static buildPerformanceStorageSection(): StorageConfiguration {
184 let storageConfiguration: StorageConfiguration = {
185 enabled: false,
186 type: StorageType.JSON_FILE,
187 uri: Configuration.getDefaultPerformanceStorageUri(StorageType.JSON_FILE),
188 };
189 if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.performanceStorage)) {
190 storageConfiguration = {
191 ...storageConfiguration,
192 ...Configuration.getConfigurationData()?.performanceStorage,
193 ...(Configuration.getConfigurationData()?.performanceStorage?.type ===
194 StorageType.JSON_FILE &&
195 Configuration.getConfigurationData()?.performanceStorage?.uri && {
196 uri: Configuration.buildPerformanceUriFilePath(
197 new URL(Configuration.getConfigurationData()!.performanceStorage!.uri!).pathname,
198 ),
199 }),
200 };
201 }
202 return storageConfiguration;
203 }
204
205 private static buildLogSection(): LogConfiguration {
206 const defaultLogConfiguration: LogConfiguration = {
207 enabled: true,
208 file: 'logs/combined.log',
209 errorFile: 'logs/error.log',
210 statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
211 level: 'info',
212 format: 'simple',
213 rotate: true,
214 };
215 const deprecatedLogConfiguration: LogConfiguration = {
216 ...(hasOwnProp(Configuration.getConfigurationData(), 'logEnabled') && {
217 enabled: Configuration.getConfigurationData()?.logEnabled,
218 }),
219 ...(hasOwnProp(Configuration.getConfigurationData(), 'logFile') && {
220 file: Configuration.getConfigurationData()?.logFile,
221 }),
222 ...(hasOwnProp(Configuration.getConfigurationData(), 'logErrorFile') && {
223 errorFile: Configuration.getConfigurationData()?.logErrorFile,
224 }),
225 ...(hasOwnProp(Configuration.getConfigurationData(), 'logStatisticsInterval') && {
226 statisticsInterval: Configuration.getConfigurationData()?.logStatisticsInterval,
227 }),
228 ...(hasOwnProp(Configuration.getConfigurationData(), 'logLevel') && {
229 level: Configuration.getConfigurationData()?.logLevel,
230 }),
231 ...(hasOwnProp(Configuration.getConfigurationData(), 'logConsole') && {
232 console: Configuration.getConfigurationData()?.logConsole,
233 }),
234 ...(hasOwnProp(Configuration.getConfigurationData(), 'logFormat') && {
235 format: Configuration.getConfigurationData()?.logFormat,
236 }),
237 ...(hasOwnProp(Configuration.getConfigurationData(), 'logRotate') && {
238 rotate: Configuration.getConfigurationData()?.logRotate,
239 }),
240 ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxFiles') && {
241 maxFiles: Configuration.getConfigurationData()?.logMaxFiles,
242 }),
243 ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxSize') && {
244 maxSize: Configuration.getConfigurationData()?.logMaxSize,
245 }),
246 };
247 const logConfiguration: LogConfiguration = {
248 ...defaultLogConfiguration,
249 ...deprecatedLogConfiguration,
250 ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.log) &&
251 Configuration.getConfigurationData()?.log),
252 };
253 return logConfiguration;
254 }
255
256 private static buildWorkerSection(): WorkerConfiguration {
257 const defaultWorkerConfiguration: WorkerConfiguration = {
258 processType: WorkerProcessType.workerSet,
259 startDelay: DEFAULT_WORKER_START_DELAY,
260 elementsPerWorker: 'auto',
261 elementStartDelay: DEFAULT_ELEMENT_START_DELAY,
262 poolMinSize: DEFAULT_POOL_MIN_SIZE,
263 poolMaxSize: DEFAULT_POOL_MAX_SIZE,
264 };
265 const deprecatedWorkerConfiguration: WorkerConfiguration = {
266 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerProcess') && {
267 processType: Configuration.getConfigurationData()?.workerProcess,
268 }),
269 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerStartDelay') && {
270 startDelay: Configuration.getConfigurationData()?.workerStartDelay,
271 }),
272 ...(hasOwnProp(Configuration.getConfigurationData(), 'chargingStationsPerWorker') && {
273 elementsPerWorker: Configuration.getConfigurationData()?.chargingStationsPerWorker,
274 }),
275 ...(hasOwnProp(Configuration.getConfigurationData(), 'elementStartDelay') && {
276 elementStartDelay: Configuration.getConfigurationData()?.elementStartDelay,
277 }),
278 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMinSize') && {
279 poolMinSize: Configuration.getConfigurationData()?.workerPoolMinSize,
280 }),
281 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMaxSize') && {
282 poolMaxSize: Configuration.getConfigurationData()?.workerPoolMaxSize,
283 }),
284 };
285 hasOwnProp(Configuration.getConfigurationData(), 'workerPoolStrategy') &&
286 delete Configuration.getConfigurationData()?.workerPoolStrategy;
287 const workerConfiguration: WorkerConfiguration = {
288 ...defaultWorkerConfiguration,
289 ...deprecatedWorkerConfiguration,
290 ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.worker) &&
291 Configuration.getConfigurationData()?.worker),
292 };
293 if (!Object.values(WorkerProcessType).includes(workerConfiguration.processType!)) {
294 throw new SyntaxError(
295 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
296 );
297 }
298 return workerConfiguration;
299 }
300
301 private static checkDeprecatedConfigurationKeys() {
302 // connection timeout
303 Configuration.warnDeprecatedConfigurationKey(
304 'autoReconnectTimeout',
305 undefined,
306 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
307 );
308 Configuration.warnDeprecatedConfigurationKey(
309 'connectionTimeout',
310 undefined,
311 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
312 );
313 // connection retries
314 Configuration.warnDeprecatedConfigurationKey(
315 'autoReconnectMaxRetries',
316 undefined,
317 'Use it in charging station template instead',
318 );
319 // station template url(s)
320 Configuration.warnDeprecatedConfigurationKey(
321 'stationTemplateURLs',
322 undefined,
323 "Use 'stationTemplateUrls' instead",
324 );
325 !isUndefined(
326 Configuration.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData],
327 ) &&
328 (Configuration.getConfigurationData()!.stationTemplateUrls =
329 Configuration.getConfigurationData()![
330 'stationTemplateURLs' as keyof ConfigurationData
331 ] as StationTemplateUrl[]);
332 Configuration.getConfigurationData()?.stationTemplateUrls.forEach(
333 (stationTemplateUrl: StationTemplateUrl) => {
334 if (!isUndefined(stationTemplateUrl?.['numberOfStation' as keyof StationTemplateUrl])) {
335 console.error(
336 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
337 `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`,
338 )}`,
339 );
340 }
341 },
342 );
343 // supervision url(s)
344 Configuration.warnDeprecatedConfigurationKey(
345 'supervisionURLs',
346 undefined,
347 "Use 'supervisionUrls' instead",
348 );
349 // supervision urls distribution
350 Configuration.warnDeprecatedConfigurationKey(
351 'distributeStationToTenantEqually',
352 undefined,
353 "Use 'supervisionUrlDistribution' instead",
354 );
355 Configuration.warnDeprecatedConfigurationKey(
356 'distributeStationsToTenantsEqually',
357 undefined,
358 "Use 'supervisionUrlDistribution' instead",
359 );
360 // worker section
361 Configuration.warnDeprecatedConfigurationKey(
362 'useWorkerPool',
363 undefined,
364 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
365 );
366 Configuration.warnDeprecatedConfigurationKey(
367 'workerProcess',
368 undefined,
369 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
370 );
371 Configuration.warnDeprecatedConfigurationKey(
372 'workerStartDelay',
373 undefined,
374 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
375 );
376 Configuration.warnDeprecatedConfigurationKey(
377 'chargingStationsPerWorker',
378 undefined,
379 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
380 );
381 Configuration.warnDeprecatedConfigurationKey(
382 'elementStartDelay',
383 undefined,
384 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
385 );
386 Configuration.warnDeprecatedConfigurationKey(
387 'workerPoolMinSize',
388 undefined,
389 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
390 );
391 Configuration.warnDeprecatedConfigurationKey(
392 'workerPoolSize;',
393 undefined,
394 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
395 );
396 Configuration.warnDeprecatedConfigurationKey(
397 'workerPoolMaxSize;',
398 undefined,
399 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
400 );
401 Configuration.warnDeprecatedConfigurationKey(
402 'workerPoolStrategy;',
403 undefined,
404 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
405 );
406 Configuration.warnDeprecatedConfigurationKey(
407 'poolStrategy',
408 ConfigurationSection.worker,
409 'Not publicly exposed to end users',
410 );
411 // log section
412 Configuration.warnDeprecatedConfigurationKey(
413 'logEnabled',
414 undefined,
415 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
416 );
417 Configuration.warnDeprecatedConfigurationKey(
418 'logFile',
419 undefined,
420 `Use '${ConfigurationSection.log}' section to define the log file instead`,
421 );
422 Configuration.warnDeprecatedConfigurationKey(
423 'logErrorFile',
424 undefined,
425 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
426 );
427 Configuration.warnDeprecatedConfigurationKey(
428 'logConsole',
429 undefined,
430 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
431 );
432 Configuration.warnDeprecatedConfigurationKey(
433 'logStatisticsInterval',
434 undefined,
435 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
436 );
437 Configuration.warnDeprecatedConfigurationKey(
438 'logLevel',
439 undefined,
440 `Use '${ConfigurationSection.log}' section to define the log level instead`,
441 );
442 Configuration.warnDeprecatedConfigurationKey(
443 'logFormat',
444 undefined,
445 `Use '${ConfigurationSection.log}' section to define the log format instead`,
446 );
447 Configuration.warnDeprecatedConfigurationKey(
448 'logRotate',
449 undefined,
450 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
451 );
452 Configuration.warnDeprecatedConfigurationKey(
453 'logMaxFiles',
454 undefined,
455 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
456 );
457 Configuration.warnDeprecatedConfigurationKey(
458 'logMaxSize',
459 undefined,
460 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
461 );
462 // performanceStorage section
463 Configuration.warnDeprecatedConfigurationKey(
464 'URI',
465 ConfigurationSection.performanceStorage,
466 "Use 'uri' instead",
467 );
468 // uiServer section
469 if (hasOwnProp(Configuration.getConfigurationData(), 'uiWebSocketServer')) {
470 console.error(
471 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
472 `Deprecated configuration section 'uiWebSocketServer' usage. Use '${ConfigurationSection.uiServer}' instead`,
473 )}`,
474 );
475 }
476 }
477
478 private static warnDeprecatedConfigurationKey(
479 key: string,
480 sectionName?: string,
481 logMsgToAppend = '',
482 ) {
483 if (
484 sectionName &&
485 !isUndefined(
486 Configuration.getConfigurationData()?.[sectionName as keyof ConfigurationData],
487 ) &&
488 !isUndefined(
489 (
490 Configuration.getConfigurationData()?.[sectionName as keyof ConfigurationData] as Record<
491 string,
492 unknown
493 >
494 )?.[key],
495 )
496 ) {
497 console.error(
498 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
499 `Deprecated configuration key '${key}' usage in section '${sectionName}'${
500 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
501 }`,
502 )}`,
503 );
504 } else if (
505 !isUndefined(Configuration.getConfigurationData()?.[key as keyof ConfigurationData])
506 ) {
507 console.error(
508 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
509 `Deprecated configuration key '${key}' usage${
510 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
511 }`,
512 )}`,
513 );
514 }
515 }
516
517 private static getConfigurationData(): ConfigurationData | undefined {
518 if (!Configuration.configurationData) {
519 try {
520 Configuration.configurationData = JSON.parse(
521 readFileSync(Configuration.configurationFile, 'utf8'),
522 ) as ConfigurationData;
523 if (!Configuration.configurationFileWatcher) {
524 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
525 }
526 } catch (error) {
527 Configuration.handleFileException(
528 Configuration.configurationFile,
529 FileType.Configuration,
530 error as NodeJS.ErrnoException,
531 Configuration.logPrefix(),
532 );
533 }
534 }
535 return Configuration.configurationData;
536 }
537
538 private static getConfigurationFileWatcher(): FSWatcher | undefined {
539 try {
540 return watch(Configuration.configurationFile, (event, filename): void => {
541 if (filename!.trim()!.length > 0 && event === 'change') {
542 delete Configuration.configurationData;
543 Configuration.configurationSectionCache.clear();
544 if (!isUndefined(Configuration.configurationChangeCallback)) {
545 Configuration.configurationChangeCallback!().catch((error) => {
546 throw typeof error === 'string' ? new Error(error) : error;
547 });
548 }
549 }
550 });
551 } catch (error) {
552 Configuration.handleFileException(
553 Configuration.configurationFile,
554 FileType.Configuration,
555 error as NodeJS.ErrnoException,
556 Configuration.logPrefix(),
557 );
558 }
559 }
560
561 private static handleFileException(
562 file: string,
563 fileType: FileType,
564 error: NodeJS.ErrnoException,
565 logPfx: string,
566 ): void {
567 const prefix = isNotEmptyString(logPfx) ? `${logPfx} ` : '';
568 let logMsg: string;
569 switch (error.code) {
570 case 'ENOENT':
571 logMsg = `${fileType} file ${file} not found: `;
572 break;
573 case 'EEXIST':
574 logMsg = `${fileType} file ${file} already exists: `;
575 break;
576 case 'EACCES':
577 logMsg = `${fileType} file ${file} access denied: `;
578 break;
579 case 'EPERM':
580 logMsg = `${fileType} file ${file} permission denied: `;
581 break;
582 default:
583 logMsg = `${fileType} file ${file} error: `;
584 }
585 console.error(`${chalk.green(prefix)}${chalk.red(logMsg)}`, error);
586 throw error;
587 }
588
589 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
590 switch (storageType) {
591 case StorageType.JSON_FILE:
592 return Configuration.buildPerformanceUriFilePath(
593 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`,
594 );
595 case StorageType.SQLITE:
596 return Configuration.buildPerformanceUriFilePath(
597 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`,
598 );
599 default:
600 throw new Error(`Unsupported storage type '${storageType}'`);
601 }
602 }
603
604 private static buildPerformanceUriFilePath(file: string) {
605 return `file://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;
606 }
607 }