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