1 | /*
|
2 | * Licensed under the Apache License, Version 2.0 (the "License");
|
3 | * you may not use this file except in compliance with the License.
|
4 | * You may obtain a copy of the License at
|
5 | *
|
6 | * http://www.apache.org/licenses/LICENSE-2.0
|
7 | *
|
8 | * Unless required by applicable law or agreed to in writing, software
|
9 | * distributed under the License is distributed on an "AS IS" BASIS,
|
10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
11 | * See the License for the specific language governing permissions and
|
12 | * limitations under the License.
|
13 | */
|
14 |
|
15 | ;
|
16 |
|
17 | const ErgoCompiler = require('./compiler');
|
18 | const Script = require('./script');
|
19 | const ParseException = require('@accordproject/concerto-cto').ParseException;
|
20 | const CompilerException = require('./compilerexception');
|
21 | const TypeException = require('./typeexception');
|
22 | const SystemException = require('./systemexception');
|
23 |
|
24 | /**
|
25 | * <p>
|
26 | * Manages a set of scripts.
|
27 | * </p>
|
28 | * @private
|
29 | * @class
|
30 | * @memberof module:ergo-compiler
|
31 | */
|
32 | class ScriptManager {
|
33 |
|
34 | /**
|
35 | * Create the ScriptManager.
|
36 | * <p>
|
37 | * <strong>Note: Only to be called by framework code. Applications should
|
38 | * retrieve instances from {@link BusinessNetworkDefinition}</strong>
|
39 | * </p>
|
40 | * @param {String} target - compiler target (either: 'es6', or 'java')
|
41 | * @param {ModelManager} modelManager - The ModelManager to use for this ScriptManager
|
42 | * @param {Object} options - e.g., { warnings: true }
|
43 | */
|
44 | constructor(target, modelManager, options) {
|
45 | this.target = target;
|
46 | this.modelManager = modelManager;
|
47 | this.scripts = {};
|
48 | this.compiledScript = null;
|
49 | this.warnings = options && options.warnings || false;
|
50 | this.sourceTemplates = [];
|
51 | }
|
52 |
|
53 | /**
|
54 | * Change the compilation target. Note: This might force recompilation if logic has already been compiled.
|
55 | * @param {String} target - compiler target (either: 'es6', or 'java')
|
56 | * @param {boolean} recompile - whether to force recompilation of the logic
|
57 | */
|
58 | changeTarget(target, recompile) {
|
59 | this.target = target;
|
60 | if (recompile) { this.compileLogic(true); }
|
61 | }
|
62 |
|
63 | /**
|
64 | * Creates a new Script from a string.
|
65 | *
|
66 | * @param {string} identifier - the identifier of the script
|
67 | * @param {string} language - the language identifier of the script
|
68 | * @param {string} contents - the contents of the script
|
69 | * @returns {Script} - the instantiated script
|
70 | */
|
71 | createScript(identifier, language, contents) {
|
72 | return new Script(this.modelManager, identifier, language, contents);
|
73 | }
|
74 |
|
75 | /**
|
76 | * Modify an existing Script from a string.
|
77 | *
|
78 | * @param {string} identifier - the identifier of the script
|
79 | * @param {string} language - the language identifier of the script
|
80 | * @param {string} contents - the contents of the script
|
81 | */
|
82 | modifyScript(identifier, language, contents) {
|
83 | this.updateScript(new Script(this.modelManager, identifier, language, contents));
|
84 | }
|
85 |
|
86 | /**
|
87 | * Adds a template file (as a string) to the ScriptManager.
|
88 | * @param {string} templateFile - The template file as a string
|
89 | * @param {string} fileName - an optional file name to associate with the template file
|
90 | */
|
91 | addTemplateFile(templateFile,fileName) {
|
92 | this.sourceTemplates.push({ 'name' : fileName, 'content': templateFile });
|
93 | }
|
94 |
|
95 | /**
|
96 | * Adds a Script to the ScriptManager
|
97 | * @param {Script} script - The script to add to the ScriptManager
|
98 | */
|
99 | addScript(script) {
|
100 | this.scripts[script.getIdentifier()] = script;
|
101 | }
|
102 |
|
103 | /**
|
104 | * Update an existing Script in the ScriptManager
|
105 | * @param {Script} script - The script to add to the ScriptManager
|
106 | */
|
107 | updateScript(script) {
|
108 | if (!this.scripts[script.getIdentifier()]) {
|
109 | throw new Error('Script file does not exist');
|
110 | }
|
111 | this.addScript(script);
|
112 | }
|
113 |
|
114 | /**
|
115 | * Remove the Script
|
116 | * @param {string} identifier - The identifier of the script to remove
|
117 | * delete.
|
118 | */
|
119 | deleteScript(identifier) {
|
120 | if (!this.scripts[identifier]) {
|
121 | throw new Error('Script file does not exist');
|
122 | }
|
123 | delete this.scripts[identifier];
|
124 | }
|
125 |
|
126 | /**
|
127 | * Get the array of Script instances
|
128 | * @return {Script[]} The Scripts registered
|
129 | * @private
|
130 | */
|
131 | getScripts() {
|
132 | let keys = Object.keys(this.scripts);
|
133 | let result = [];
|
134 |
|
135 | for(let n=0; n < keys.length;n++) {
|
136 | result.push(this.scripts[keys[n]]);
|
137 | }
|
138 |
|
139 | return result;
|
140 | }
|
141 |
|
142 | /**
|
143 | * Get the array of all Script instances, including compiled ones
|
144 | * @return {Script[]} The Scripts registered, including compiled ones
|
145 | * @private
|
146 | */
|
147 | getAllScripts() {
|
148 | let result = this.getScripts();
|
149 | if (this.compiledScript !== null) {
|
150 | result.push(this.compiledScript);
|
151 | }
|
152 | return result;
|
153 | }
|
154 |
|
155 | /**
|
156 | * Get a single combined Script
|
157 | * @return {string} The source for all Scripts registered, including compiled ones
|
158 | * @private
|
159 | */
|
160 | getCombinedScripts() {
|
161 | let allJsScripts = '';
|
162 |
|
163 | this.getAllScripts().forEach(function (element) {
|
164 | if (element.getLanguage() === '.js') {
|
165 | allJsScripts += element.getContents();
|
166 | }
|
167 | }, this);
|
168 |
|
169 | return allJsScripts;
|
170 | }
|
171 |
|
172 | /**
|
173 | * Target kind
|
174 | * @param {string} target - the target language
|
175 | * @return {string} the kind of language ('.js', '.ergo', '.java')
|
176 | */
|
177 | getTargetKind(target) {
|
178 | if (target === 'ergo') {
|
179 | return '.ergo';
|
180 | } else if (target === 'java') {
|
181 | return '.java';
|
182 | } else {
|
183 | return '.js';
|
184 | }
|
185 | }
|
186 |
|
187 | /**
|
188 | * Get the array of Script instances for the given language
|
189 | * @param {string} target - the target language
|
190 | * @return {Script[]} The Scripts registered
|
191 | * @private
|
192 | */
|
193 | getScriptsForTarget(target) {
|
194 | const language = this.getTargetKind(target);
|
195 | const scripts = this.getAllScripts();
|
196 | let keys = Object.keys(scripts);
|
197 | let result = [];
|
198 |
|
199 | for(let n=0; n < keys.length;n++) {
|
200 | if (scripts[keys[n]].getLanguage() === language) {
|
201 | result.push(scripts[keys[n]]);
|
202 | }
|
203 | }
|
204 | return result;
|
205 | }
|
206 |
|
207 | /**
|
208 | * Gets all the Ergo logic
|
209 | * @return {Array<{name:string, content:string}>} the name and content of each Ergo file
|
210 | */
|
211 | getLogic() {
|
212 | let logic = [];
|
213 | const scripts = this.getScriptsForTarget('ergo');
|
214 | scripts.forEach(function (script) {
|
215 | logic.push({ 'name' : script.getIdentifier(), 'content' : script.getContents() });
|
216 | });
|
217 | return logic;
|
218 | }
|
219 |
|
220 | /**
|
221 | * Remove all registered scripts
|
222 | */
|
223 | clearScripts() {
|
224 | this.scripts = {};
|
225 | this.compiledScript = null;
|
226 | }
|
227 |
|
228 | /**
|
229 | * Get the Script associated with an identifier
|
230 | * @param {string} identifier - the identifier of the Script
|
231 | * @return {Script} the Script
|
232 | * @private
|
233 | */
|
234 | getScript(identifier) {
|
235 | return this.scripts[identifier];
|
236 | }
|
237 |
|
238 | /**
|
239 | * Get the compiled Script
|
240 | * @return {Script} the Script
|
241 | * @private
|
242 | */
|
243 | getCompiledScript() {
|
244 | return this.compileLogic(false);
|
245 | }
|
246 |
|
247 | /**
|
248 | * Get the compiled JavaScript
|
249 | * @return {string} the Script
|
250 | * @private
|
251 | */
|
252 | getCompiledJavaScript() {
|
253 | const compiledScript = this.compiledScript;
|
254 | let allJsScripts = '';
|
255 |
|
256 | if (compiledScript) {
|
257 | allJsScripts += compiledScript.getContents();
|
258 | } else {
|
259 | throw new Error('Did not find any compiled JavaScript logic');
|
260 | }
|
261 |
|
262 | return allJsScripts;
|
263 | }
|
264 |
|
265 | /**
|
266 | * Get the identifiers of all registered scripts
|
267 | * @return {string[]} The identifiers of all registered scripts
|
268 | */
|
269 | getScriptIdentifiers() {
|
270 | return Object.keys(this.scripts);
|
271 | }
|
272 |
|
273 | /**
|
274 | * Throw the right kind of error
|
275 | * @param {object} error - Ergo compiler error
|
276 | * @throws {BaseFileException}
|
277 | */
|
278 | static _throwCompilerException(error) {
|
279 | let fileLocation = {};
|
280 |
|
281 | // Convert from Ergo file location to Concerto file location
|
282 | fileLocation.start = {
|
283 | line: error.locstart.line,
|
284 | column: error.locstart.column
|
285 | };
|
286 | fileLocation.end = {
|
287 | line: error.locend.line,
|
288 | column: error.locend.column
|
289 | };
|
290 |
|
291 | if (error.kind === 'CompilationError') {
|
292 | throw new CompilerException(error.message, fileLocation, error.fullMessage, error.fileName);
|
293 | } else if (error.kind === 'ParseError') {
|
294 | throw new ParseException(error.message, fileLocation, error.fileName, error.fullMessage, 'ergo-compiler');
|
295 | } else if (error.kind === 'TypeError') {
|
296 | throw new TypeException(error.message, fileLocation, error.fullMessage, error.fileName);
|
297 | } else {
|
298 | throw new SystemException(error.message, fileLocation, error.fullMessage, error.fileName);
|
299 | }
|
300 | }
|
301 |
|
302 | /**
|
303 | * Compile the Ergo logic
|
304 | * @param {boolean} force - whether to force recompilation of the logic
|
305 | * @return {object} The script compiled to JavaScript
|
306 | */
|
307 | compileLogic(force) {
|
308 | if (this.compiledScript && !force) {
|
309 | return this.compiledScript;
|
310 | }
|
311 | const codeExt = this.target === 'java' ? '.java' : '.js';
|
312 | let sourceErgo = this.getLogic();
|
313 | if (sourceErgo === undefined || sourceErgo.length === 0 && this.sourceTemplates.length === 0) {
|
314 | const allJsScripts = this.getCombinedScripts();
|
315 | if (allJsScripts === '') {
|
316 | return null;
|
317 | }
|
318 | this.compiledScript = new Script(this.modelManager, 'main'+codeExt, codeExt, allJsScripts, null);
|
319 | } else {
|
320 | // Do not link to runtime for Java target, only for JavaScript
|
321 | const link = this.target === 'java' ? false : true;
|
322 | const compiledErgo = ErgoCompiler.compileToJavaScript(sourceErgo,this.modelManager.getModels(),this.sourceTemplates,this.target,link,this.warnings);
|
323 | if (Object.prototype.hasOwnProperty.call(compiledErgo,'error')) {
|
324 | ScriptManager._throwCompilerException(compiledErgo.error);
|
325 | }
|
326 | this.compiledScript = new Script(this.modelManager, 'main'+codeExt, codeExt, compiledErgo.success, compiledErgo.contractName);
|
327 | }
|
328 | return this.compiledScript;
|
329 | }
|
330 |
|
331 | /**
|
332 | * Helper method to retrieve all function declarations
|
333 | * @returns {Array} a list of function declarations
|
334 | */
|
335 | allFunctionDeclarations() {
|
336 | let allScripts = this.getAllScripts();
|
337 | const functionDeclarations = allScripts
|
338 | .map((ele) => {
|
339 | return ele.getFunctionDeclarations();
|
340 | }).reduce((flat, next) => {
|
341 | return flat.concat(next);
|
342 | },[]);
|
343 | return functionDeclarations;
|
344 | }
|
345 |
|
346 | /**
|
347 | * Looks for the presence of a function in the JavaScript logic
|
348 | * @param {string} name - the function name
|
349 | */
|
350 | hasFunctionDeclaration(name) {
|
351 | // get the function declarations of either init or dispatch
|
352 | const funDecls = this.allFunctionDeclarations();
|
353 | if (!funDecls.some((ele) => { return ele.getName() === name; })) {
|
354 | throw new Error(`Function ${name} was not found in logic`);
|
355 | }
|
356 | }
|
357 | /**
|
358 | * Checks that the logic has a dispatch function
|
359 | */
|
360 | hasDispatch() {
|
361 | this.hasFunctionDeclaration('__dispatch');
|
362 | }
|
363 |
|
364 | /**
|
365 | * Checks that the logic has an init function
|
366 | */
|
367 | hasInit() {
|
368 | this.hasFunctionDeclaration('__init');
|
369 | }
|
370 |
|
371 | }
|
372 |
|
373 | module.exports = ScriptManager;
|