profoundjs 6.0.0-beta.4 → 6.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,405 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ /*
6
+
7
+ * This script is intended to be run by the user at an interactive shell after a fresh installation.
8
+
9
+ * Prompts for configuration values and other installation options.
10
+ * Creates config.js
11
+ * Kicks off 'npm run setup' to complete installation.
12
+
13
+ */
14
+
15
+ const child_process = require("child_process");
16
+ const fs = require("fs");
17
+ const os = require("os");
18
+ const path = require("path");
19
+ const readline = require("readline");
20
+ const iutils = require(path.join(__dirname, "node_modules", "profoundjs", "setup", "install_utils.js"));
21
+
22
+ const IBMi = os.type() === "OS400";
23
+
24
+ (async () => {
25
+ try {
26
+
27
+ const deployDir = iutils.getDeployDir();
28
+ if (!deployDir) {
29
+ console.error("Can't find deployment directory.");
30
+ process.exit(1);
31
+ }
32
+ const configPath = iutils.getConfigPath();
33
+ let config = fileExists(configPath) ? require(configPath) : {};
34
+
35
+ // Get configuration values.
36
+ let staticFilesDirectory, port, downloadStaticFiles, gitSupport;
37
+ let profounduiLibrary, connectorLibrary, connectorIASP;
38
+ let svrname, autostart, ccsid, nodePath;
39
+
40
+ let defaultStaticDir;
41
+ if (IBMi) {
42
+ defaultStaticDir = config.staticFilesDirectory || "/www/profoundui/htdocs"
43
+ if (!directoryExists(defaultStaticDir)) {
44
+ console.log("");
45
+ console.log("WARNING: Profound UI Installation not found in default location.");
46
+ console.log("");
47
+ }
48
+ }
49
+ else {
50
+ defaultStaticDir = config.staticFilesDirectory || "htdocs";
51
+ console.log("");
52
+ console.log("You can provide the Profound UI directory as a remote URL (e.g. http://myibmi:8080) or as a local directory name. If the directory does not exist, the installer can create it for you.");
53
+ console.log("");
54
+ }
55
+ staticFilesDirectory = await ask("Specify Profound UI static files directory", defaultStaticDir, validateDirectory);
56
+ if (!IBMi && !isURL(staticFilesDirectory) && !directoryExists(path.resolve(deployDir, staticFilesDirectory))) {
57
+ console.log("");
58
+ console.log("The specified directory for Profound UI was not found.");
59
+ console.log("");
60
+ let answer = (await ask("Should the installer create the directory and download a copy of Profound UI static files?", "y", validateYesNo)).toUpperCase();
61
+ downloadStaticFiles = (answer === "Y");
62
+ }
63
+ port = Number(await ask("Specify port number for Profound.js server", config.port || 8081, validatePort));
64
+
65
+
66
+ if (["win32", "darwin", "linux"].includes(os.platform())) {
67
+ let answer = (await ask("Install with Git integration?", config.gitSupport === false ? "n" : "y" , validateYesNo)).toUpperCase();
68
+ gitSupport = (answer === "Y");
69
+ }
70
+ if (IBMi) {
71
+ console.log("");
72
+ profounduiLibrary = (await ask("Specify Profound UI installation library", config.profounduiLibrary || "PROFOUNDUI", validateName)).toUpperCase();
73
+ let answer = (await ask("Install Profound.js Connector IBM i ILE components?", "y", validateYesNo)).toUpperCase();
74
+ if (answer === "Y") {
75
+ connectorLibrary = (await ask("Enter Profound.js Connector library name:", config.connectorLibrary || "PROFOUNDJS", validateName)).toUpperCase();
76
+ connectorIASP = (await ask("Enter Profound.js Connector library IASP:", config.connectorIASP || "*SYSBAS", validateIASP)).toUpperCase();
77
+ }
78
+ const serverInstances = iutils.getIBMiInstances(deployDir);
79
+ if (serverInstances.length > 1) {
80
+ console.log("\nWARNING - Multiple STRTCPSVR/ENDTCPSVR instances found for this server in /profoundjs-base/instances:\n");
81
+ serverInstances.forEach(serverInstance => {
82
+ console.log(JSON.stringify(serverInstance));
83
+ });
84
+ }
85
+ answer = (await ask("\nCreate/replace server instance configuration for STRTCPSVR/ENDTCPSVR commands?", "y", validateYesNo)).toUpperCase();
86
+ if (answer === "Y") {
87
+ let defaultName = "PROFOUNDJS";
88
+ const defaultOpts = {
89
+ ccsid: "37",
90
+ nodePath: process.argv[0],
91
+ autostart: "1"
92
+ };
93
+ const serverInstance = serverInstances[0];
94
+ if (serverInstance) {
95
+ defaultName = serverInstance.name;
96
+ defaultOpts.autostart = "0";
97
+ for (const name in defaultOpts) {
98
+ const opt = serverInstance.options.find(option => option.name === name);
99
+ if (opt) {
100
+ defaultOpts[name] = opt.value.trim();
101
+ }
102
+ }
103
+ }
104
+ svrname = (await ask("Server instance name:", defaultName, validateName)).toUpperCase();
105
+ ccsid = await ask("Specify CCSID for instance " + svrname, defaultOpts.ccsid, isValidCcsid);
106
+ nodePath = await ask("Specify Node.js path for instance " + svrname, defaultOpts.nodePath, isValidNodePath);
107
+ autostart = (await ask("Autostart server instance " + svrname + " when TCP/IP starts?", defaultOpts.autostart === "1" ? "y" : "n", validateYesNo)).toUpperCase();
108
+ autostart = (autostart === "Y");
109
+ }
110
+ }
111
+
112
+ // Create/update configuration file.
113
+ if (fileExists(configPath)) {
114
+
115
+ config.staticFilesDirectory = staticFilesDirectory;
116
+ config.port = port;
117
+ if (gitSupport === false) {
118
+ config.gitSupport = false;
119
+ }
120
+ else if (config.hasOwnProperty("gitSupport")) {
121
+ delete config.gitSupport;
122
+ }
123
+ if (IBMi) {
124
+ config.profounduiLibrary = profounduiLibrary;
125
+ if (connectorLibrary) {
126
+ config.connectorLibrary = connectorLibrary;
127
+ }
128
+ else {
129
+ if (config.hasOwnProperty("connectorLibrary")) {
130
+ delete config.connectorLibrary;
131
+ }
132
+ if (config.hasOwnProperty("connectorIASP")) {
133
+ delete config.connectorIASP;
134
+ }
135
+ }
136
+ if (connectorIASP && connectorIASP !== "*SYSBAS") {
137
+ config.connectorIASP = connectorIASP;
138
+ }
139
+ else if (config.hasOwnProperty("connectorIASP")) {
140
+ delete config.connectorIASP;
141
+ }
142
+ }
143
+ fs.writeFileSync(configPath, stringifyConfig(config));
144
+ console.log("");
145
+ console.log("config.js updated.");
146
+ console.log("");
147
+
148
+ }
149
+ else {
150
+
151
+ let content = fs.readFileSync(path.join(iutils.getSetupDir(), "config.js"), "utf8");
152
+ eval("config = " + content.substr(content.indexOf("{")));
153
+ config.staticFilesDirectory = staticFilesDirectory;
154
+ config.port = port;
155
+ if (connectorLibrary) {
156
+ config.connectorLibrary = connectorLibrary;
157
+ if (connectorIASP !== "*SYSBAS") {
158
+ config.connectorIASP = connectorIASP;
159
+ }
160
+ }
161
+ if (profounduiLibrary) {
162
+ config.profounduiLibrary = profounduiLibrary;
163
+ }
164
+ if (IBMi) {
165
+ config.showIBMiParmDefn = true;
166
+ }
167
+ if (gitSupport === false) {
168
+ config.gitSupport = false;
169
+ }
170
+ // Check if this is a workspace (if so, default to mysql)
171
+ if (!IBMi && fileExists(path.join(deployDir, "modules", "app", ".noderun", "settings.json"))) {
172
+ if (config.databaseConnections) delete config.databaseConnections; // remove property so that it's appended at the end when we set it
173
+ config.databaseConnections = [{
174
+ name: "default",
175
+ default: true,
176
+ driver: "mysql",
177
+ driverOptions: {
178
+ user: "user-name",
179
+ password: "your-password",
180
+ host: "localhost",
181
+ database: "database-name"
182
+ }
183
+ }];
184
+ }
185
+ content = "\nmodule.exports = ";
186
+ content += JSON.stringify(config, null, " ");
187
+ content += "\n";
188
+ fs.writeFileSync(configPath, content);
189
+ console.log("");
190
+ console.log("config.js created.");
191
+ console.log("");
192
+
193
+ }
194
+
195
+ // Complete installation.
196
+ process.stdin.destroy();
197
+ const args = [];
198
+ if (gitSupport) {
199
+ args.push("--nodegit");
200
+ }
201
+ if (downloadStaticFiles) {
202
+ args.push("--static-files");
203
+ }
204
+ if (connectorLibrary) {
205
+ args.push("--ibmi-connector-library");
206
+ }
207
+ if (svrname) {
208
+ args.push("--ibmi-instance=" + svrname);
209
+ args.push("--ibmi-instance-ccsid=" + ccsid);
210
+ args.push("--ibmi-instance-node-path=" + nodePath);
211
+ if (autostart) {
212
+ args.push("--ibmi-instance-autostart");
213
+ }
214
+ else {
215
+ args.push("--no-ibmi-instance-autostart");
216
+ }
217
+ }
218
+ try {
219
+ child_process.execSync(
220
+ `node setup.js ${args.join(" ")}`,
221
+ {
222
+ cwd: iutils.getSetupDir(),
223
+ stdio: ["ignore", "inherit", "inherit"]
224
+ }
225
+ );
226
+ }
227
+ catch (error) {
228
+ process.exit(1);
229
+ }
230
+
231
+ }
232
+ catch (error) {
233
+ process.stdin.destroy();
234
+ console.error(error);
235
+ process.exit(1);
236
+ }
237
+ })();
238
+
239
+ async function ask(questionParm, defaultAnswer, validationFunction) {
240
+
241
+ let question = questionParm;
242
+ if (defaultAnswer == null) defaultAnswer = "";
243
+ if (typeof defaultAnswer !== "string") defaultAnswer = String(defaultAnswer);
244
+
245
+ if (defaultAnswer !== "") {
246
+ let lastChar = question.substr(question.length - 1, 1);
247
+ if (lastChar === ":" || lastChar === "?") {
248
+ question = question.substr(0, question.length - 1) + " (" + defaultAnswer + ")" + lastChar;
249
+ }
250
+ else {
251
+ question = question + " (" + defaultAnswer + ")" + ":";
252
+ }
253
+ }
254
+ question += " ";
255
+
256
+ const readlineInterface = readline.createInterface({
257
+ input: process.stdin,
258
+ output: process.stdout,
259
+ terminal: false
260
+ });
261
+ let answer = await new Promise(resolve => {
262
+ readlineInterface.question(question, function(answer) {
263
+ readlineInterface.close();
264
+ resolve(answer);
265
+ });
266
+ });
267
+
268
+ if (answer == "") answer = defaultAnswer;
269
+ answer = answer.trim();
270
+ if (typeof validationFunction === "function" && validationFunction(answer) === false) {
271
+ answer = await ask(questionParm, defaultAnswer, validationFunction);
272
+ }
273
+ return answer;
274
+
275
+ }
276
+
277
+ function fileExists(file) {
278
+ var exists = false;
279
+ try {
280
+ var stat = fs.statSync(file);
281
+ if (stat && stat.isFile()) exists = true;
282
+ }
283
+ catch (err) {
284
+ exists = false;
285
+ }
286
+ return exists;
287
+ }
288
+
289
+ function directoryExists(dir) {
290
+ var exists = false;
291
+ try {
292
+ var stat = fs.statSync(dir);
293
+ if (stat && stat.isDirectory()) exists = true;
294
+ }
295
+ catch (err) {
296
+ exists = false;
297
+ }
298
+ return exists;
299
+ }
300
+
301
+ function validateName(name) {
302
+
303
+ const error = iutils.validateIBMiName(name);
304
+ if (error) {
305
+ console.log(error);
306
+ return false;
307
+ }
308
+ return true;
309
+
310
+ }
311
+
312
+ function validateIASP(name) {
313
+
314
+ const error = iutils.validateIBMiIASP(name);
315
+ if (error) {
316
+ console.log(error);
317
+ return false;
318
+ }
319
+ return true;
320
+
321
+ }
322
+
323
+ function isURL(path) {
324
+ if (typeof path !== "string") return false;
325
+ path = path.toLowerCase();
326
+ if (path.substr(0, 7) === "http://") return true;
327
+ if (path.substr(0, 8) === "https://") return true;
328
+ return false;
329
+ }
330
+
331
+ function validateDirectory(directory) {
332
+ if (IBMi) {
333
+ var ch = directory.substr(0, 1);
334
+ if (ch !== "/" && ch !== "\\" && !isURL(directory)) {
335
+ directory = path.join(iutils.getDeployDir(), path.sep, directory);
336
+ }
337
+ if (!directoryExists(directory) && !isURL(directory)) {
338
+ console.log("WARNING: The specified directory was not found. You can create it later or modify the staticFilesDirectory property in config.js.");
339
+ console.log("");
340
+ }
341
+ }
342
+ return true; // accept any entry - if directory doesn't exist, simply warn
343
+ }
344
+
345
+ function validatePort(port) {
346
+ if (port === "0") return true;
347
+ port = Number(port);
348
+ if (!isNaN(port) && port >= 1 && port <= 65535) return true;
349
+ console.log("Invalid port number. Valid range is 0-65535.");
350
+ return false;
351
+ }
352
+
353
+ function isValidNodePath(nodePath) {
354
+
355
+ if (!fileExists(nodePath)) {
356
+ console.log("Path " + nodePath + " does not exist. A valid path name must be entered.");
357
+ return false;
358
+ }
359
+ return true;
360
+ }
361
+
362
+ function isValidCcsid(ccsid) {
363
+
364
+ const error = iutils.validateIBMiCCSID(parseInt(ccsid, 10));
365
+ if (error) {
366
+ console.log(error);
367
+ return false;
368
+ }
369
+ return true;
370
+
371
+ }
372
+
373
+ function validateYesNo(answer) {
374
+ answer = answer.toUpperCase();
375
+ if (answer === "Y" || answer === "YES" || answer === "N" || answer === "NO") return true;
376
+ console.log("Invalid answer. Use y or n.");
377
+ return false;
378
+ }
379
+
380
+ function stringifyConfig(config) {
381
+
382
+ // JavaScript functions (like connectorIPFilter) will be lost when doing JSON.stringify,
383
+ // so we need to do some magic to preserve them.
384
+
385
+ let funkz = [];
386
+ let configString = "\nmodule.exports = ";
387
+
388
+ configString += JSON.stringify(config, (key, val) => {
389
+ if (typeof val === 'function') {
390
+ let n = funkz.length;
391
+ funkz.push(val.toString());
392
+ return `___function${n}___`;
393
+ }
394
+ return val;
395
+ }, " ");
396
+
397
+ configString += "\n";
398
+
399
+ for (var i=0; i<funkz.length; i++) {
400
+ configString = configString.replace(`"___function${i}___"`, funkz[i]);
401
+ }
402
+
403
+ return configString;
404
+
405
+ }
package/setup/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  module.exports = {
3
3
  "port": 8081,
4
- "staticFilesDirectory": "puisrc/htdocs",
4
+ "staticFilesDirectory": "htdocs",
5
5
  "pathlist": [
6
6
  "pjssamples"
7
7
  ],
@@ -0,0 +1,148 @@
1
+
2
+ "use strict";
3
+
4
+ /*
5
+
6
+ This is NPM lifecycle script: postinstall
7
+
8
+ * Installs additional platform-specific dependencies:
9
+ - IBMi: idb-connector
10
+ - Other platforms: profoundjs-node-pty
11
+
12
+ * Installs minimal files into the users's deployment directory:
13
+ - package.json
14
+ - start.js
15
+ - complete_install.js
16
+
17
+ * All additional installation is done by script 'npm run setup'.
18
+ - If 'config.js' is present in deployment directory, this script kicks off 'npm run setup' automatically.
19
+ - Otherwise (i.e. fresh installation) user manually runs 'complete_install.js' to configure and complete installation.
20
+
21
+ NOTE: Output from this script will be suppressed by NPM 7+, unless process ends with non-zero exit code.
22
+
23
+ */
24
+
25
+ // Platform-specific dependency versions.
26
+ const idb_connector_version = "1.2.16";
27
+ const node_pty_version = "^2";
28
+
29
+ const child_process = require("child_process");
30
+ const fs = require("fs");
31
+ const iutils = require("./install_utils.js");
32
+ const os = require("os");
33
+ const path = require("path");
34
+
35
+ const IBMi = os.type() === "OS400";
36
+
37
+ const deployDir = iutils.getDeployDir();
38
+ if (!deployDir) {
39
+ console.error("Can't find deployment directory.");
40
+ process.exit(1);
41
+ }
42
+
43
+ // Install platform-specific dependencies.
44
+ if (IBMi) {
45
+ console.log(`Installing idb-connector...`);
46
+ child_process.execSync(
47
+ `npm install --no-package-lock idb-connector@"${idb_connector_version}"`,
48
+ {
49
+ cwd: iutils.getPackageDir(),
50
+ stdio: "inherit"
51
+ }
52
+ );
53
+ }
54
+ else {
55
+ console.log(`Installing profoundjs-node-pty...`);
56
+ child_process.execSync(
57
+ `npm install --save-optional --no-package-lock profoundjs-node-pty@"${node_pty_version}"`,
58
+ {
59
+ cwd: iutils.getPackageDir(),
60
+ stdio: "inherit"
61
+ }
62
+ );
63
+ }
64
+
65
+ // Install files.
66
+ installFile("package.json");
67
+ installFile("start.js");
68
+ installFile("complete_install.js", true);
69
+
70
+ // If config.js is present, run 'npm run setup' to complete installation.
71
+ const configPath = iutils.getConfigPath();
72
+ if (fileExists(configPath)) {
73
+ console.log("\nconfig.js exists. Completing installation.\n");
74
+ const config = require(configPath);
75
+ let args = [];
76
+ if (IBMi) {
77
+ if (config.connectorLibrary !== undefined) {
78
+ args.push("--ibmi-connector-library");
79
+ }
80
+ const instances = iutils.getIBMiInstances(deployDir);
81
+ if (instances.length > 0) {
82
+ const instance = instances[0];
83
+ args.push("--ibmi-instance=" + instance.name);
84
+ let opt = getOpt("autostart");
85
+ if (opt && opt.value === "1") {
86
+ args.push("--ibmi-instance-autostart");
87
+ }
88
+ opt = getOpt("ccsid");
89
+ if (opt) {
90
+ args.push("--ibmi-instance-ccsid=" + opt.value);
91
+ }
92
+ opt = getOpt("nodePath");
93
+ if (opt) {
94
+ args.push("--ibmi-instance-node-path=" + opt.value);
95
+ }
96
+ function getOpt(name) {
97
+ return instance.options.find(opt => opt.name === name);
98
+ }
99
+ }
100
+ }
101
+ if (["win32", "darwin", "linux"].includes(os.platform()) && config.gitSupport !== false) {
102
+ args.push("--nodegit");
103
+ }
104
+ if (args.length === 0) {
105
+ args = "";
106
+ }
107
+ else {
108
+ args = "-- " + args.join(" ");
109
+ }
110
+ child_process.execSync(
111
+ `npm run setup ${args}`,
112
+ {
113
+ cwd: iutils.getPackageDir(),
114
+ stdio: ["ignore", "inherit", "inherit"]
115
+ }
116
+ );
117
+ }
118
+ else {
119
+ console.log("\nconfig.js does not exist.");
120
+ console.log("To configure and complete installation, run: node complete_install.js\n");
121
+ }
122
+
123
+ function fileExists(file) {
124
+ try {
125
+ const stats = fs.statSync(file);
126
+ return stats.isFile();
127
+ }
128
+ catch (error) {
129
+ return false;
130
+ }
131
+ }
132
+
133
+ function installFile(file, overwrite) {
134
+ const dest = path.join(deployDir, file);
135
+ if (overwrite !== true && fileExists(dest)) {
136
+ console.log(`${file} exists.`);
137
+ }
138
+ else {
139
+ fs.writeFileSync(
140
+ dest,
141
+ fs.readFileSync(path.join(__dirname, file)),
142
+ {
143
+ flag: overwrite === true ? "w" : "wx"
144
+ }
145
+ );
146
+ console.log(`${file} created.`);
147
+ }
148
+ }
@@ -0,0 +1,123 @@
1
+
2
+ "use strict";
3
+
4
+ /*
5
+ Utilities for PJS install/setup process.
6
+ */
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+
11
+ exports.getConfigPath = function() {
12
+
13
+ return path.join(exports.getDeployDir(), "config.js");
14
+
15
+ }
16
+
17
+ exports.getDeployDir = function() {
18
+
19
+ const dirParts = __dirname.split(path.sep);
20
+ while (dirParts.length > 0 && dirParts.pop() !== "node_modules") {};
21
+ if (dirParts.length > 0) {
22
+ return dirParts.join(path.sep);
23
+ }
24
+
25
+ }
26
+
27
+ exports.getPackageDir = function() {
28
+
29
+ return path.resolve(__dirname, "..");
30
+
31
+ }
32
+
33
+ exports.getSetupDir = function() {
34
+
35
+ return path.join(exports.getPackageDir(), "setup");
36
+
37
+ }
38
+
39
+ exports.getIBMiInstances = function(installDir) {
40
+
41
+ const INSTANCES_DIR = "/profoundjs-base/instances";
42
+
43
+ if (!fs.existsSync(INSTANCES_DIR)) {
44
+ return [];
45
+ }
46
+
47
+ const dirs = fs.readdirSync(
48
+ INSTANCES_DIR,
49
+ {
50
+ withFileTypes: true
51
+ }
52
+ )
53
+ .filter(el => el.isDirectory());
54
+
55
+ let instances = [];
56
+ for (const dir of dirs) {
57
+ let conf;
58
+ try {
59
+ conf = fs.readFileSync(path.join(INSTANCES_DIR, dir.name, "conf"), "utf8");
60
+ }
61
+ catch (error) {
62
+ continue;
63
+ }
64
+ const instance ={
65
+ name: dir.name.toUpperCase(),
66
+ options: []
67
+ };
68
+ const lines = conf.split("\n");
69
+ for (const line of lines) {
70
+ const parts = line.split("=");
71
+ if (parts.length >= 2) {
72
+ instance.options.push({
73
+ name: parts[0].trim(),
74
+ value: parts.slice(1).join("=")
75
+ });
76
+ }
77
+ }
78
+ instances.push(instance);
79
+ }
80
+ if (installDir) {
81
+ instances = instances.filter(instance => {
82
+ const startFile = instance.options.find(option => option.name === "path");
83
+ return startFile && startFile.value.trim() === path.join(installDir, "start.js");
84
+ });
85
+ }
86
+ return instances;
87
+
88
+ }
89
+
90
+ exports.validateIBMiCCSID = function(ccsid) {
91
+
92
+ if (!Number.isInteger(ccsid) || ccsid < 1 || ccsid > 65535) {
93
+ return "CCSID must be a number 1-65535";
94
+ }
95
+
96
+ }
97
+
98
+ exports.validateIBMiIASP = function(name) {
99
+
100
+ if (name.toUpperCase() !== "*SYSBAS") {
101
+ return exports.validateIBMiName(name);
102
+ }
103
+
104
+ }
105
+
106
+ exports.validateIBMiName = function(name) {
107
+
108
+ if (name.length < 1 || name.length > 10) {
109
+ return "Name must be from 1-10 characters.";
110
+ }
111
+
112
+ const firstChar = name.substr(0, 1).toUpperCase();
113
+ if ((firstChar < "A" || firstChar > "Z") && firstChar != "#" && firstChar != "@" && firstChar != "$") {
114
+ return "Name must start with either a character A-Z, #, @, or $";
115
+ }
116
+ for (let i = 1; i < name.length; i++) {
117
+ const chr = name.substr(i, 1).toUpperCase();
118
+ if ((chr < "A" || chr > "Z") && (chr < "0" || chr > "9") && chr != "#" && chr != "@" && chr != "$" && chr != "_" && chr != ".") {
119
+ return "Name contains an invalid character: " + chr;
120
+ }
121
+ }
122
+
123
+ }