UI Server: Add support for cloud foundry deployment
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
1 import fs from 'fs';
2 import path from 'path';
3 import { fileURLToPath } from 'url';
4
5 import chalk from 'chalk';
6
7 import {
8 type ConfigurationData,
9 type StationTemplateUrl,
10 type StorageConfiguration,
11 SupervisionUrlDistribution,
12 type UIServerConfiguration,
13 type WorkerConfiguration,
14 } from '../types/ConfigurationData';
15 import type { EmptyObject } from '../types/EmptyObject';
16 import type { HandleErrorParams } from '../types/Error';
17 import { FileType } from '../types/FileType';
18 import { StorageType } from '../types/Storage';
19 import { ApplicationProtocol } from '../types/UIProtocol';
20 import { WorkerProcessType } from '../types/Worker';
21 import WorkerConstants from '../worker/WorkerConstants';
22 import Constants from './Constants';
23
24 export default class Configuration {
25 private static configurationFile = path.join(
26 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
27 'assets',
28 'config.json'
29 );
30
31 private static configurationFileWatcher: fs.FSWatcher;
32 private static configuration: ConfigurationData | null = null;
33 private static configurationChangeCallback: () => Promise<void>;
34
35 private constructor() {
36 // This is intentional
37 }
38
39 static setConfigurationChangeCallback(cb: () => Promise<void>): void {
40 Configuration.configurationChangeCallback = cb;
41 }
42
43 static getLogStatisticsInterval(): number {
44 Configuration.warnDeprecatedConfigurationKey(
45 'statisticsDisplayInterval',
46 null,
47 "Use 'logStatisticsInterval' instead"
48 );
49 // Read conf
50 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logStatisticsInterval')
51 ? Configuration.getConfig().logStatisticsInterval
52 : Constants.DEFAULT_LOG_STATISTICS_INTERVAL;
53 }
54
55 static getUIServer(): UIServerConfiguration {
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 }
61 let uiServerConfiguration: UIServerConfiguration = {
62 enabled: false,
63 type: ApplicationProtocol.WS,
64 options: {
65 host: Constants.DEFAULT_UI_WEBSOCKET_SERVER_HOST,
66 port: Constants.DEFAULT_UI_WEBSOCKET_SERVER_PORT,
67 },
68 };
69 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'uiServer')) {
70 uiServerConfiguration = Configuration.deepMerge(
71 uiServerConfiguration,
72 Configuration.getConfig().uiServer
73 );
74 }
75 if (Configuration.isCFEnvironment() === true) {
76 delete uiServerConfiguration.options.host;
77 uiServerConfiguration.options.port = parseInt(process.env.PORT);
78 }
79 return uiServerConfiguration;
80 }
81
82 static getPerformanceStorage(): StorageConfiguration {
83 Configuration.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
84 let storageConfiguration: StorageConfiguration = {
85 enabled: false,
86 type: StorageType.JSON_FILE,
87 uri: this.getDefaultPerformanceStorageUri(StorageType.JSON_FILE),
88 };
89 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'performanceStorage')) {
90 storageConfiguration = {
91 ...storageConfiguration,
92 ...Configuration.getConfig().performanceStorage,
93 };
94 }
95 return storageConfiguration;
96 }
97
98 static getAutoReconnectMaxRetries(): number {
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 );
114 // Read conf
115 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'autoReconnectMaxRetries')) {
116 return Configuration.getConfig().autoReconnectMaxRetries;
117 }
118 }
119
120 static getStationTemplateUrls(): StationTemplateUrl[] {
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[]);
130 Configuration.getConfig().stationTemplateUrls.forEach((stationUrl: StationTemplateUrl) => {
131 if (!Configuration.isUndefined(stationUrl['numberOfStation'])) {
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 );
137 }
138 });
139 // Read conf
140 return Configuration.getConfig().stationTemplateUrls;
141 }
142
143 static getWorker(): WorkerConfiguration {
144 Configuration.warnDeprecatedConfigurationKey(
145 'useWorkerPool',
146 null,
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"
173 );
174 Configuration.warnDeprecatedConfigurationKey(
175 'workerPoolSize;',
176 null,
177 "Use 'worker' section to define the worker pool maximum size instead"
178 );
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 );
189 let workerConfiguration: WorkerConfiguration = {
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')) {
223 workerConfiguration = { ...workerConfiguration, ...Configuration.getConfig().worker };
224 }
225 return workerConfiguration;
226 }
227
228 static getLogConsole(): boolean {
229 Configuration.warnDeprecatedConfigurationKey('consoleLog', null, "Use 'logConsole' instead");
230 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logConsole')
231 ? Configuration.getConfig().logConsole
232 : false;
233 }
234
235 static getLogFormat(): string {
236 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logFormat')
237 ? Configuration.getConfig().logFormat
238 : 'simple';
239 }
240
241 static getLogRotate(): boolean {
242 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logRotate')
243 ? Configuration.getConfig().logRotate
244 : true;
245 }
246
247 static getLogMaxFiles(): number {
248 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logMaxFiles')
249 ? Configuration.getConfig().logMaxFiles
250 : 7;
251 }
252
253 static getLogLevel(): string {
254 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logLevel')
255 ? Configuration.getConfig().logLevel.toLowerCase()
256 : 'info';
257 }
258
259 static getLogFile(): string {
260 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logFile')
261 ? Configuration.getConfig().logFile
262 : 'combined.log';
263 }
264
265 static getLogErrorFile(): string {
266 Configuration.warnDeprecatedConfigurationKey('errorFile', null, "Use 'logErrorFile' instead");
267 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logErrorFile')
268 ? Configuration.getConfig().logErrorFile
269 : 'error.log';
270 }
271
272 static getSupervisionUrls(): string | string[] {
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[]);
282 // Read conf
283 return Configuration.getConfig().supervisionUrls;
284 }
285
286 static getSupervisionUrlDistribution(): SupervisionUrlDistribution {
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;
303 }
304
305 private static logPrefix(): string {
306 return new Date().toLocaleString() + ' Simulator configuration |';
307 }
308
309 private static warnDeprecatedConfigurationKey(
310 key: string,
311 sectionName?: string,
312 logMsgToAppend = ''
313 ) {
314 if (
315 sectionName &&
316 !Configuration.isUndefined(Configuration.getConfig()[sectionName]) &&
317 !Configuration.isUndefined(
318 (Configuration.getConfig()[sectionName] as Record<string, unknown>)[key]
319 )
320 ) {
321 console.error(
322 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage in section '${sectionName}'${
323 logMsgToAppend && '. ' + logMsgToAppend
324 }}`
325 );
326 } else if (!Configuration.isUndefined(Configuration.getConfig()[key])) {
327 console.error(
328 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage${
329 logMsgToAppend && '. ' + logMsgToAppend
330 }}`
331 );
332 }
333 }
334
335 // Read the config file
336 private static getConfig(): ConfigurationData {
337 if (!Configuration.configuration) {
338 try {
339 Configuration.configuration = JSON.parse(
340 fs.readFileSync(Configuration.configurationFile, 'utf8')
341 ) as ConfigurationData;
342 } catch (error) {
343 Configuration.handleFileException(
344 Configuration.logPrefix(),
345 FileType.Configuration,
346 Configuration.configurationFile,
347 error as NodeJS.ErrnoException
348 );
349 }
350 if (!Configuration.configurationFileWatcher) {
351 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
352 }
353 }
354 return Configuration.configuration;
355 }
356
357 private static getConfigurationFileWatcher(): fs.FSWatcher {
358 try {
359 return fs.watch(Configuration.configurationFile, (event, filename): void => {
360 if (filename && event === 'change') {
361 // Nullify to force configuration file reading
362 Configuration.configuration = null;
363 if (!Configuration.isUndefined(Configuration.configurationChangeCallback)) {
364 Configuration.configurationChangeCallback().catch((error) => {
365 throw typeof error === 'string' ? new Error(error) : error;
366 });
367 }
368 }
369 });
370 } catch (error) {
371 Configuration.handleFileException(
372 Configuration.logPrefix(),
373 FileType.Configuration,
374 Configuration.configurationFile,
375 error as NodeJS.ErrnoException
376 );
377 }
378 }
379
380 private static isCFEnvironment(): boolean {
381 return process.env.VCAP_APPLICATION !== undefined;
382 }
383
384 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
385 const SQLiteFileName = `${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`;
386 switch (storageType) {
387 case StorageType.JSON_FILE:
388 return `file://${path.join(
389 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
390 Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME
391 )}`;
392 case StorageType.SQLITE:
393 return `file://${path.join(
394 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
395 SQLiteFileName
396 )}`;
397 default:
398 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
399 }
400 }
401
402 private static isObject(item): boolean {
403 return item && typeof item === 'object' && Array.isArray(item) === false;
404 }
405
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
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
437 private static handleFileException(
438 logPrefix: string,
439 fileType: FileType,
440 filePath: string,
441 error: NodeJS.ErrnoException,
442 params: HandleErrorParams<EmptyObject> = { throwError: true }
443 ): void {
444 const prefix = logPrefix.length !== 0 ? logPrefix + ' ' : '';
445 if (error.code === 'ENOENT') {
446 console.error(
447 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' not found: '),
448 error
449 );
450 } else if (error.code === 'EEXIST') {
451 console.error(
452 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' already exists: '),
453 error
454 );
455 } else if (error.code === 'EACCES') {
456 console.error(
457 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' access denied: '),
458 error
459 );
460 } else {
461 console.error(
462 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' error: '),
463 error
464 );
465 }
466 if (params?.throwError) {
467 throw error;
468 }
469 }
470 }