UI Server: Add support for cloud foundry deployment
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
CommitLineData
8114d10e
JB
1import fs from 'fs';
2import path from 'path';
3import { fileURLToPath } from 'url';
4
5import chalk from 'chalk';
6
83e00df1
JB
7import {
8 type ConfigurationData,
9 type StationTemplateUrl,
10 type StorageConfiguration,
e7aeea18 11 SupervisionUrlDistribution,
83e00df1
JB
12 type UIServerConfiguration,
13 type WorkerConfiguration,
e7aeea18 14} from '../types/ConfigurationData';
6c1761d4
JB
15import type { EmptyObject } from '../types/EmptyObject';
16import type { HandleErrorParams } from '../types/Error';
8114d10e 17import { FileType } from '../types/FileType';
72f041bd 18import { StorageType } from '../types/Storage';
1f7fa4de 19import { ApplicationProtocol } from '../types/UIProtocol';
a4624c96 20import { WorkerProcessType } from '../types/Worker';
8114d10e
JB
21import WorkerConstants from '../worker/WorkerConstants';
22import Constants from './Constants';
7dde0b73 23
3f40bc9c 24export default class Configuration {
a95873d8 25 private static configurationFile = path.join(
0d8140bd 26 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
e7aeea18
JB
27 'assets',
28 'config.json'
29 );
10068088 30
ded13d97 31 private static configurationFileWatcher: fs.FSWatcher;
6e0964c8 32 private static configuration: ConfigurationData | null = null;
e57acf6a
JB
33 private static configurationChangeCallback: () => Promise<void>;
34
d5bd1c00
JB
35 private constructor() {
36 // This is intentional
37 }
38
e57acf6a
JB
39 static setConfigurationChangeCallback(cb: () => Promise<void>): void {
40 Configuration.configurationChangeCallback = cb;
41 }
7dde0b73 42
72f041bd 43 static getLogStatisticsInterval(): number {
e7aeea18
JB
44 Configuration.warnDeprecatedConfigurationKey(
45 'statisticsDisplayInterval',
46 null,
47 "Use 'logStatisticsInterval' instead"
48 );
7dde0b73 49 // Read conf
e7aeea18
JB
50 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logStatisticsInterval')
51 ? Configuration.getConfig().logStatisticsInterval
25f5a959 52 : Constants.DEFAULT_LOG_STATISTICS_INTERVAL;
72f041bd
JB
53 }
54
675fa8e3 55 static getUIServer(): UIServerConfiguration {
66271092
JB
56 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'uiWebSocketServer')) {
57 console.error(
58 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration section 'uiWebSocketServer' usage. Use 'uiServer' instead}`
59 );
60 }
675fa8e3 61 let uiServerConfiguration: UIServerConfiguration = {
b803eefa 62 enabled: false,
1f7fa4de 63 type: ApplicationProtocol.WS,
c127bd64
JB
64 options: {
65 host: Constants.DEFAULT_UI_WEBSOCKET_SERVER_HOST,
66 port: Constants.DEFAULT_UI_WEBSOCKET_SERVER_PORT,
67 },
6a49ad23 68 };
675fa8e3 69 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'uiServer')) {
d5bd1c00
JB
70 uiServerConfiguration = Configuration.deepMerge(
71 uiServerConfiguration,
72 Configuration.getConfig().uiServer
73 );
6a49ad23 74 }
b803eefa
JB
75 if (Configuration.isCFEnvironment() === true) {
76 delete uiServerConfiguration.options.host;
77 uiServerConfiguration.options.port = parseInt(process.env.PORT);
78 }
675fa8e3 79 return uiServerConfiguration;
6a49ad23
JB
80 }
81
72f041bd 82 static getPerformanceStorage(): StorageConfiguration {
e7aeea18 83 Configuration.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
6a49ad23
JB
84 let storageConfiguration: StorageConfiguration = {
85 enabled: false,
86 type: StorageType.JSON_FILE,
e7aeea18 87 uri: this.getDefaultPerformanceStorageUri(StorageType.JSON_FILE),
6a49ad23 88 };
72f041bd 89 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'performanceStorage')) {
e7aeea18 90 storageConfiguration = {
1ba1e8fb 91 ...storageConfiguration,
c127bd64 92 ...Configuration.getConfig().performanceStorage,
72f041bd 93 };
72f041bd
JB
94 }
95 return storageConfiguration;
7dde0b73
JB
96 }
97
9ccca265 98 static getAutoReconnectMaxRetries(): number {
e7aeea18
JB
99 Configuration.warnDeprecatedConfigurationKey(
100 'autoReconnectTimeout',
101 null,
102 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
103 );
104 Configuration.warnDeprecatedConfigurationKey(
105 'connectionTimeout',
106 null,
107 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
108 );
109 Configuration.warnDeprecatedConfigurationKey(
110 'autoReconnectMaxRetries',
111 null,
112 'Use it in charging station template instead'
113 );
7dde0b73 114 // Read conf
963ee397 115 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'autoReconnectMaxRetries')) {
3574dfd3
JB
116 return Configuration.getConfig().autoReconnectMaxRetries;
117 }
7dde0b73
JB
118 }
119
1f5df42a 120 static getStationTemplateUrls(): StationTemplateUrl[] {
e7aeea18
JB
121 Configuration.warnDeprecatedConfigurationKey(
122 'stationTemplateURLs',
123 null,
124 "Use 'stationTemplateUrls' instead"
125 );
126 !Configuration.isUndefined(Configuration.getConfig()['stationTemplateURLs']) &&
127 (Configuration.getConfig().stationTemplateUrls = Configuration.getConfig()[
128 'stationTemplateURLs'
129 ] as StationTemplateUrl[]);
1f5df42a
JB
130 Configuration.getConfig().stationTemplateUrls.forEach((stationUrl: StationTemplateUrl) => {
131 if (!Configuration.isUndefined(stationUrl['numberOfStation'])) {
e7aeea18
JB
132 console.error(
133 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key 'numberOfStation' usage for template file '${
134 stationUrl.file
135 }' in 'stationTemplateUrls'. Use 'numberOfStations' instead}`
136 );
eb3937cb
JB
137 }
138 });
7dde0b73 139 // Read conf
1f5df42a 140 return Configuration.getConfig().stationTemplateUrls;
7dde0b73
JB
141 }
142
cf2a5d9b 143 static getWorker(): WorkerConfiguration {
e7aeea18 144 Configuration.warnDeprecatedConfigurationKey(
e80bc579 145 'useWorkerPool',
e7aeea18 146 null,
cf2a5d9b
JB
147 "Use 'worker' section to define the type of worker process model instead"
148 );
149 Configuration.warnDeprecatedConfigurationKey(
150 'workerProcess',
151 null,
152 "Use 'worker' section to define the type of worker process model instead"
153 );
154 Configuration.warnDeprecatedConfigurationKey(
155 'workerStartDelay',
156 null,
157 "Use 'worker' section to define the worker start delay instead"
158 );
159 Configuration.warnDeprecatedConfigurationKey(
160 'chargingStationsPerWorker',
161 null,
162 "Use 'worker' section to define the number of element(s) per worker instead"
163 );
164 Configuration.warnDeprecatedConfigurationKey(
165 'elementStartDelay',
166 null,
167 "Use 'worker' section to define the worker's element start delay instead"
168 );
169 Configuration.warnDeprecatedConfigurationKey(
170 'workerPoolMinSize',
171 null,
172 "Use 'worker' section to define the worker pool minimum size instead"
e7aeea18 173 );
e7aeea18
JB
174 Configuration.warnDeprecatedConfigurationKey(
175 'workerPoolSize;',
176 null,
cf2a5d9b 177 "Use 'worker' section to define the worker pool maximum size instead"
e7aeea18 178 );
cf2a5d9b
JB
179 Configuration.warnDeprecatedConfigurationKey(
180 'workerPoolMaxSize;',
181 null,
182 "Use 'worker' section to define the worker pool maximum size instead"
183 );
184 Configuration.warnDeprecatedConfigurationKey(
185 'workerPoolStrategy;',
186 null,
187 "Use 'worker' section to define the worker pool strategy instead"
188 );
c127bd64 189 let workerConfiguration: WorkerConfiguration = {
cf2a5d9b
JB
190 processType: Configuration.objectHasOwnProperty(Configuration.getConfig(), 'workerProcess')
191 ? Configuration.getConfig().workerProcess
192 : WorkerProcessType.WORKER_SET,
193 startDelay: Configuration.objectHasOwnProperty(Configuration.getConfig(), 'workerStartDelay')
194 ? Configuration.getConfig().workerStartDelay
195 : WorkerConstants.DEFAULT_WORKER_START_DELAY,
196 elementsPerWorker: Configuration.objectHasOwnProperty(
197 Configuration.getConfig(),
198 'chargingStationsPerWorker'
199 )
200 ? Configuration.getConfig().chargingStationsPerWorker
201 : WorkerConstants.DEFAULT_ELEMENTS_PER_WORKER,
202 elementStartDelay: Configuration.objectHasOwnProperty(
203 Configuration.getConfig(),
204 'elementStartDelay'
205 )
206 ? Configuration.getConfig().elementStartDelay
207 : WorkerConstants.DEFAULT_ELEMENT_START_DELAY,
208 poolMinSize: Configuration.objectHasOwnProperty(
209 Configuration.getConfig(),
210 'workerPoolMinSize'
211 )
212 ? Configuration.getConfig().workerPoolMinSize
213 : WorkerConstants.DEFAULT_POOL_MIN_SIZE,
214 poolMaxSize: Configuration.objectHasOwnProperty(
215 Configuration.getConfig(),
216 'workerPoolMaxSize'
217 )
218 ? Configuration.getConfig().workerPoolMaxSize
219 : WorkerConstants.DEFAULT_POOL_MAX_SIZE,
220 poolStrategy: Configuration.getConfig().workerPoolStrategy,
221 };
222 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'worker')) {
c127bd64 223 workerConfiguration = { ...workerConfiguration, ...Configuration.getConfig().worker };
cf2a5d9b
JB
224 }
225 return workerConfiguration;
3d2ff9e4
J
226 }
227
7ec46a9a 228 static getLogConsole(): boolean {
e7aeea18
JB
229 Configuration.warnDeprecatedConfigurationKey('consoleLog', null, "Use 'logConsole' instead");
230 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logConsole')
231 ? Configuration.getConfig().logConsole
232 : false;
7dde0b73
JB
233 }
234
a4a21709 235 static getLogFormat(): string {
e7aeea18
JB
236 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logFormat')
237 ? Configuration.getConfig().logFormat
238 : 'simple';
027b409a
JB
239 }
240
6bf6769e 241 static getLogRotate(): boolean {
e7aeea18
JB
242 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logRotate')
243 ? Configuration.getConfig().logRotate
244 : true;
6bf6769e
JB
245 }
246
247 static getLogMaxFiles(): number {
e7aeea18
JB
248 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logMaxFiles')
249 ? Configuration.getConfig().logMaxFiles
250 : 7;
6bf6769e
JB
251 }
252
324fd4ee 253 static getLogLevel(): string {
e7aeea18
JB
254 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logLevel')
255 ? Configuration.getConfig().logLevel.toLowerCase()
256 : 'info';
2e6f5966
JB
257 }
258
a4a21709 259 static getLogFile(): string {
e7aeea18
JB
260 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logFile')
261 ? Configuration.getConfig().logFile
262 : 'combined.log';
7dde0b73
JB
263 }
264
7ec46a9a 265 static getLogErrorFile(): string {
e7aeea18
JB
266 Configuration.warnDeprecatedConfigurationKey('errorFile', null, "Use 'logErrorFile' instead");
267 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logErrorFile')
268 ? Configuration.getConfig().logErrorFile
269 : 'error.log';
7dde0b73
JB
270 }
271
2dcfe98e 272 static getSupervisionUrls(): string | string[] {
e7aeea18
JB
273 Configuration.warnDeprecatedConfigurationKey(
274 'supervisionURLs',
275 null,
276 "Use 'supervisionUrls' instead"
277 );
278 !Configuration.isUndefined(Configuration.getConfig()['supervisionURLs']) &&
279 (Configuration.getConfig().supervisionUrls = Configuration.getConfig()[
280 'supervisionURLs'
281 ] as string[]);
7dde0b73 282 // Read conf
1f5df42a 283 return Configuration.getConfig().supervisionUrls;
7dde0b73
JB
284 }
285
2dcfe98e 286 static getSupervisionUrlDistribution(): SupervisionUrlDistribution {
e7aeea18
JB
287 Configuration.warnDeprecatedConfigurationKey(
288 'distributeStationToTenantEqually',
289 null,
290 "Use 'supervisionUrlDistribution' instead"
291 );
292 Configuration.warnDeprecatedConfigurationKey(
293 'distributeStationsToTenantsEqually',
294 null,
295 "Use 'supervisionUrlDistribution' instead"
296 );
297 return Configuration.objectHasOwnProperty(
298 Configuration.getConfig(),
299 'supervisionUrlDistribution'
300 )
301 ? Configuration.getConfig().supervisionUrlDistribution
302 : SupervisionUrlDistribution.ROUND_ROBIN;
7dde0b73 303 }
eb3937cb 304
23132a44
JB
305 private static logPrefix(): string {
306 return new Date().toLocaleString() + ' Simulator configuration |';
307 }
308
e7aeea18
JB
309 private static warnDeprecatedConfigurationKey(
310 key: string,
311 sectionName?: string,
312 logMsgToAppend = ''
313 ) {
e7aeea18
JB
314 if (
315 sectionName &&
316 !Configuration.isUndefined(Configuration.getConfig()[sectionName]) &&
455ee9cf
JB
317 !Configuration.isUndefined(
318 (Configuration.getConfig()[sectionName] as Record<string, unknown>)[key]
319 )
e7aeea18
JB
320 ) {
321 console.error(
322 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage in section '${sectionName}'${
323 logMsgToAppend && '. ' + logMsgToAppend
324 }}`
325 );
912136b1 326 } else if (!Configuration.isUndefined(Configuration.getConfig()[key])) {
e7aeea18
JB
327 console.error(
328 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage${
329 logMsgToAppend && '. ' + logMsgToAppend
330 }}`
331 );
eb3937cb
JB
332 }
333 }
334
335 // Read the config file
336 private static getConfig(): ConfigurationData {
337 if (!Configuration.configuration) {
23132a44 338 try {
e7aeea18 339 Configuration.configuration = JSON.parse(
a95873d8 340 fs.readFileSync(Configuration.configurationFile, 'utf8')
e7aeea18 341 ) as ConfigurationData;
23132a44 342 } catch (error) {
e7aeea18
JB
343 Configuration.handleFileException(
344 Configuration.logPrefix(),
a95873d8
JB
345 FileType.Configuration,
346 Configuration.configurationFile,
3fa0f0ed 347 error as NodeJS.ErrnoException
e7aeea18 348 );
23132a44 349 }
ded13d97
JB
350 if (!Configuration.configurationFileWatcher) {
351 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
352 }
eb3937cb
JB
353 }
354 return Configuration.configuration;
355 }
963ee397 356
ded13d97 357 private static getConfigurationFileWatcher(): fs.FSWatcher {
23132a44 358 try {
a95873d8 359 return fs.watch(Configuration.configurationFile, (event, filename): void => {
3ec10737
JB
360 if (filename && event === 'change') {
361 // Nullify to force configuration file reading
362 Configuration.configuration = null;
363 if (!Configuration.isUndefined(Configuration.configurationChangeCallback)) {
dcaf96dc
JB
364 Configuration.configurationChangeCallback().catch((error) => {
365 throw typeof error === 'string' ? new Error(error) : error;
366 });
3ec10737 367 }
23132a44
JB
368 }
369 });
370 } catch (error) {
e7aeea18
JB
371 Configuration.handleFileException(
372 Configuration.logPrefix(),
a95873d8
JB
373 FileType.Configuration,
374 Configuration.configurationFile,
375 error as NodeJS.ErrnoException
e7aeea18 376 );
23132a44 377 }
ded13d97
JB
378 }
379
b803eefa
JB
380 private static isCFEnvironment(): boolean {
381 return process.env.VCAP_APPLICATION !== undefined;
382 }
383
1f5df42a 384 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
d5603918
JB
385 const SQLiteFileName = `${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`;
386 switch (storageType) {
387 case StorageType.JSON_FILE:
e7aeea18 388 return `file://${path.join(
0d8140bd 389 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
e7aeea18
JB
390 Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME
391 )}`;
d5603918 392 case StorageType.SQLITE:
0d8140bd
JB
393 return `file://${path.join(
394 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
395 SQLiteFileName
396 )}`;
d5603918
JB
397 default:
398 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
399 }
400 }
401
d5bd1c00 402 private static isObject(item): boolean {
53e5fd67 403 return item && typeof item === 'object' && Array.isArray(item) === false;
d5bd1c00
JB
404 }
405
b803eefa
JB
406 private static objectHasOwnProperty(object: unknown, property: string): boolean {
407 return Object.prototype.hasOwnProperty.call(object, property) as boolean;
408 }
409
410 private static isUndefined(obj: unknown): boolean {
411 return typeof obj === 'undefined';
412 }
413
d5bd1c00
JB
414 private static deepMerge(target: object, ...sources: object[]): object {
415 if (!sources.length) {
416 return target;
417 }
418 const source = sources.shift();
419
420 if (Configuration.isObject(target) && Configuration.isObject(source)) {
421 for (const key in source) {
422 if (Configuration.isObject(source[key])) {
423 if (!target[key]) {
424 Object.assign(target, { [key]: {} });
425 }
426 // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
427 Configuration.deepMerge(target[key], source[key]);
428 } else {
429 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
430 Object.assign(target, { [key]: source[key] });
431 }
432 }
433 }
434 return Configuration.deepMerge(target, ...sources);
435 }
436
e7aeea18
JB
437 private static handleFileException(
438 logPrefix: string,
a95873d8 439 fileType: FileType,
e7aeea18
JB
440 filePath: string,
441 error: NodeJS.ErrnoException,
442 params: HandleErrorParams<EmptyObject> = { throwError: true }
443 ): void {
23132a44
JB
444 const prefix = logPrefix.length !== 0 ? logPrefix + ' ' : '';
445 if (error.code === 'ENOENT') {
e7aeea18
JB
446 console.error(
447 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' not found: '),
448 error
449 );
72f041bd 450 } else if (error.code === 'EEXIST') {
e7aeea18
JB
451 console.error(
452 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' already exists: '),
453 error
454 );
72f041bd 455 } else if (error.code === 'EACCES') {
e7aeea18
JB
456 console.error(
457 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' access denied: '),
458 error
459 );
23132a44 460 } else {
e7aeea18
JB
461 console.error(
462 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' error: '),
463 error
464 );
23132a44 465 }
e0a50bcd
JB
466 if (params?.throwError) {
467 throw error;
468 }
23132a44 469 }
7dde0b73 470}