/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*/
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.thingml.networkplugins.js;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.WriterConfig;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.sintef.thingml.*;
import org.sintef.thingml.constraints.ThingMLHelpers;
import org.sintef.thingml.helpers.AnnotatedElementHelper;
import org.sintef.thingml.helpers.ConfigurationHelper;
import org.sintef.thingml.helpers.PrimitiveTyperHelper;
import org.sintef.thingml.helpers.ThingHelper;
import org.thingml.compilers.Context;
import org.thingml.compilers.javascript.JSCfgMainGenerator;
import org.thingml.compilers.javascript.JSCompiler;
import org.thingml.compilers.spi.NetworkPlugin;
import java.io.*;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.*;
public class JSKevoreePlugin extends NetworkPlugin {
public JSKevoreePlugin() {
super();
}
public String getPluginID() {
return "JSKevoreePlugin";
}
public List<String> getSupportedProtocols() {
List<String> res = new ArrayList<>();
res.add("kevoree");
res.add("Kevoree");
res.add("kev");
res.add("Kev");
return res;
}
public List<String> getTargetedLanguages() {
List<String> res = new ArrayList<>();
res.add("nodejs");
return res;
}
public void generateNetworkLibrary(Configuration cfg, Context ctx, Set<Protocol> protocols) {
//ctx.getCompiler().processDebug(cfg);
updatePackageJSON(ctx, cfg);
generateGruntFile(ctx);
generateWrapper(ctx, cfg);
generateKevScript(ctx, cfg);
}
protected void generateKevScript(Context ctx, Configuration cfg) {
if (AnnotatedElementHelper.hasAnnotation(cfg, "kevscript")) {
final String path = AnnotatedElementHelper.annotation(cfg, "kevscript").get(0);
final URI uri = URI.create(path);
File toCopy = null;
if(uri.isAbsolute()) {
toCopy = new File(uri);
} else {
toCopy = new File(ctx.getCompiler().currentFile.toURI().resolve(path));
}
try {
FileUtils.copyFile(toCopy, new File(ctx.getOutputDirectory(), "/kevs/main.kevs"));
return;
} catch (IOException e) {
System.err.println("Cannot find file " + AnnotatedElementHelper.annotation(cfg, "kevscript").get(0) + ". Will create a new one from scratch instead");
}
}
StringBuilder kevScript = ctx.getBuilder("/kevs/main.kevs");
kevScript.append("//create a default JavaScript node\n");
kevScript.append("add node0 : JavascriptNode/LATEST/LATEST\n");
kevScript.append("//create a default group to manage the node(s)\n");
kevScript.append("add sync : CentralizedWSGroup/LATEST/LATEST\n");
kevScript.append("attach node0 sync\n\n");
kevScript.append("set sync.isMaster/node0 = 'true'\n");
kevScript.append("//instantiate Kevoree/ThingML components\n");
kevScript.append("add node0." + cfg.getName() + "_0 : thingml." + cfg.getName() + "/1/LATEST\n");//TODO: allow customizing version number and namespace
for (String k : AnnotatedElementHelper.annotation(ctx.getCurrentConfiguration(), "kevscript_import")) {
kevScript.append(k);
}
if (AnnotatedElementHelper.hasAnnotation(ctx.getCurrentConfiguration(), "kevscript_import"))
kevScript.append("\n");
kevScript.append("\n");
PrintWriter w = null;
try {
new File(ctx.getOutputDirectory() + "/kevs").mkdirs();
w = new PrintWriter(new FileWriter(new File(ctx.getOutputDirectory() + "/kevs/main.kevs")));
w.println(kevScript);
w.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(w);
}
}
protected void generateGruntFile(Context ctx) {
//copy Gruntfile.js
try {
final InputStream input = this.getClass().getClassLoader().getResourceAsStream("javascript/lib/Gruntfile.js");
final List<String> pomLines = IOUtils.readLines(input, Charset.forName("UTF-8"));
String pom = "";
for (String line : pomLines) {
pom += line + "\n";
}
input.close();
String dep = "";
int i = 0;
for (String d : AnnotatedElementHelper.annotation(ctx.getCurrentConfiguration(), "kevoree_import")) {
if (i > 0)
dep += ", ";
dep += "'" + ctx.getOutputDirectory().getParentFile().getAbsolutePath().replace("\\", "/") + "/" + d + "'";
i++;
}
pom = pom.replace("mergeLocalLibraries: []", "mergeLocalLibraries: [" + dep + "]");
final PrintWriter w = new PrintWriter(new FileWriter(new File(ctx.getOutputDirectory() + "/Gruntfile.js")));
w.println(pom);
w.close();
} catch (Exception e) {
e.printStackTrace();
}
}
protected void updatePackageJSON(Context ctx, Configuration cfg) {
//Update package.json
try {
final InputStream input = new FileInputStream(ctx.getOutputDirectory() + "/package.json");
final List<String> packLines = IOUtils.readLines(input);
String pack = "";
for (String line : packLines) {
pack += line + "\n";
}
input.close();
final JsonObject json = JsonObject.readFrom(pack);
json.set("main", "lib/" + cfg.getName() + ".js");
final JsonObject deps = json.get("dependencies").asObject();
deps.add("kevoree-entities", "^9.0.0");
final JsonObject devDeps = json.get("devDependencies").asObject();
devDeps.add("grunt", "^1");
devDeps.add("grunt-kevoree", "^5");
devDeps.add("grunt-kevoree-genmodel", "^3");
devDeps.add("grunt-kevoree-registry", "^3");
devDeps.add("grunt-kevoree-registry", "^3");
devDeps.add("eslint", "^3.11.1");
devDeps.add("load-grunt-tasks", "^3");
final JsonObject scripts = json.get("scripts").asObject();
scripts.add("prepublish", "npm run lint && grunt");
scripts.add("postpublish", "grunt publish");
scripts.add("lint", "eslint lib");
//TODO: read description from annotation
final JsonObject kevProp = JsonObject.readFrom("{\"namespace\":\"thingml\"}");//TODO: read namespace from annotation
json.add("kevoree", kevProp);
final File f = new File(ctx.getOutputDirectory() + "/package.json");
final OutputStream output = new FileOutputStream(f);
IOUtils.write(json.toString(WriterConfig.PRETTY_PRINT), output, Charset.forName("UTF-8"));
IOUtils.closeQuietly(output);
} catch (Exception e) {
e.printStackTrace();
}
}
protected String getVariableName(Instance i, Property p, Context ctx) {
return "dic_" + i.getName() + "_" + ctx.getVariableName(p);
}
protected String getGlobalVariableName(Property p, Context ctx) {
return "dic_" + ctx.getVariableName(p);
}
protected void generateRequires(StringBuilder builder, Context ctx, Configuration cfg) {
builder.append("const AbstractComponent = require('kevoree-entities/lib/AbstractComponent');\n");
if(!((JSCompiler)ctx.getCompiler()).multiThreaded) {
for (Thing t : ConfigurationHelper.allThings(cfg)) {
builder.append("const " + t.getName() + " = require('./" + t.getName() + "');\n");
}
}
}
protected void generateConstruct(StringBuilder builder, Context ctx, Configuration cfg) {
JSCfgMainGenerator.generateInstances(cfg, builder, ctx, true);
JSCfgMainGenerator.generateConnectors(cfg, builder, ctx, true);
for (Map.Entry<Instance, List<Port>> e : ConfigurationHelper.danglingPorts(cfg).entrySet()) {
final Instance i = e.getKey();
for (Port p : e.getValue()) {
for (Message m : p.getSends()) {
builder.append("this." + i.getName() + ".bus.on('" + p.getName() + "?" + m.getName() + "', (msg) => setImmediate(() => this." + shortName(i, p, m) + "_proxy(msg)));\n");
}
}
}
for (ExternalConnector c : ConfigurationHelper.getExternalConnectors(cfg)) {
if (c.getProtocol().getName().equals("kevoree")) {
final Instance i = c.getInst().getInstance();
for (Message m : c.getPort().getSends()) {
final Port p = c.getPort();
builder.append("this." + i.getName() + ".bus.on('" + p.getName() + "?" + m.getName() + "', (msg) => setImmediate(() => this." + shortName(i, p, m) + "_proxy(msg)));\n");
}
}
}
}
protected void generateStart(StringBuilder builder, Context ctx, Configuration cfg) {
for (Instance i : ConfigurationHelper.allInstances(cfg)) {
for (Property p : ThingHelper.allUsedProperties(i.getType())) {
if (p.isChangeable() && p.getCardinality() == null && p.getType() instanceof PrimitiveType && p.eContainer() instanceof Thing) {
String accessor = "getValue";
boolean isNumber = false;
if (PrimitiveTyperHelper.isNumber(((PrimitiveType) p.getType()))) {
accessor = "getNumber";
isNumber = true;
}
if (AnnotatedElementHelper.isDefined(p, "kevoree", "instance")) {
generateKevoreeListener(builder, ctx, isNumber, p, i, false, accessor);
generateThingMLListener(builder, ctx, p, i, accessor, false);
} else if (AnnotatedElementHelper.isDefined(p, "kevoree", "merge")) {
generateKevoreeListener(builder, ctx, isNumber, p, i, true, accessor);//FIXME: should generate one listener that update all thingml attribute, rather than n listeners on the same attribute that update one thingml attribute...
generateThingMLListener(builder, ctx, p, i, accessor, true);
}
}
}
}
for (Instance i : ConfigurationHelper.allInstances(cfg)) {
builder.append("this." + i.getName() + "._init();\n");
}
builder.append("done();\n");
}
protected void generatePorts(StringBuilder builder, Context ctx, Configuration cfg) {
for (Map.Entry e : ConfigurationHelper.danglingPorts(cfg).entrySet()) {
final Instance i = (Instance) e.getKey();
for (Port p : (List<Port>) e.getValue()) {
if (!p.getReceives().isEmpty()) {
builder.append(",\nin_" + shortName(i, p, null) + "_in: function (msg) {//Dangling ThingML port " + p.getName() + " (handling all incoming messages)\n");
int j = 0;
for (Message m : p.getReceives()) {
if (j > 0)
builder.append("else ");
builder.append("if(msg.split('@:@')[0] === '" + m.getName() + "'){\n");
builder.append("this." + i.getName() + ".receive" + m.getName() + "On" + p.getName() + "(");
for (Parameter pa : m.getParameters()) {
if (m.getParameters().indexOf(pa) > 0)
builder.append(", ");
builder.append("msg.split('@:@')[1].split(';')[" + m.getParameters().indexOf(pa) + "]");
}
builder.append(");\n}\n");
j++;
}
builder.append("}");
}
if (!p.getSends().isEmpty()) {
for (Message m : p.getSends()) {
builder.append(",\n" + shortName(i, p, m) + "_proxy: function() {//Dangling ThingML port " + p.getName() + " (handler for message " + m.getName() + ")\nthis.out_" + shortName(i, p, null) + "_out(");
builder.append("'" + m.getName() + "@:@'");
for (Parameter pa : m.getParameters()) {
builder.append(" + arguments[" + m.getParameters().indexOf(pa) + "] + ';'");
}
builder.append(");}");
}
builder.append(",\nout_" + shortName(i, p, null) + "_out: function() {/* Kevoree required port (out) for dangling ThingML port " + p.getName() + "\nThis will be overwritten @runtime by Kevoree JS */}");
}
}
}
for (ExternalConnector c : ConfigurationHelper.getExternalConnectors(cfg)) { //External kevoree port should be split (to allow easy integration with external non-HEADS services)
if (c.getProtocol().getName().equals("kevoree")) {
final Instance i = c.getInst().getInstance();
for (Message m : c.getPort().getReceives()) {
builder.append(",\nin_" + shortName(i, c.getPort(), m) + "_in: function (msg) {//@protocol \"kevoree\" for message " + m.getName() + " on port " + c.getPort().getName() + "\n");
builder.append("this." + i.getName() + ".receive" + m.getName() + "On" + c.getPort().getName() + "(msg.split(';'));\n");
builder.append("}");
}
for (Message m : c.getPort().getSends()) {
builder.append(",\n" + shortName(i, c.getPort(), m) + "_proxy: function() {//@protocol \"kevoree\" for message " + m.getName() + " on port " + c.getPort().getName() + "\nthis.out_" + shortName(i, c.getPort(), m) + "_out(");
int index;
for (index = 0; index < m.getParameters().size(); index++) {
if (index > 0)
builder.append(" + ';' + ");
builder.append("arguments[" + index + "]");
}
if (index > 1)
builder.append("''");
builder.append(");}");
builder.append(",\nout_" + shortName(i, c.getPort(), m) + "_out: function(msg) {/* This will be overwritten @runtime by Kevoree JS */}");
}
}
}
}
protected void generateStop(StringBuilder builder, Context ctx, Configuration cfg) {
for (Instance i : ConfigurationHelper.allInstances(cfg)) {
builder.append("this." + i.getName() + "._stop();\n");
}
builder.append("done();\n");
}
private void generateWrapper(Context ctx, Configuration cfg) {
//Generate wrapper
//Move all .js file (previously generated) into lib folder
//final File dir = new File(ctx.getOutputDirectory() + "/" + cfg.getName());
final File lib = new File(ctx.getOutputDirectory(), "lib");
lib.mkdirs();
for (File f : ctx.getOutputDirectory().listFiles()) {
if (FilenameUtils.getExtension(f.getAbsolutePath()).equals("js") && !f.getName().equals("Gruntfile.js")) {
try {
FileUtils.moveFileToDirectory(f, lib, false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
final StringBuilder builder = ctx.getBuilder("/lib/" + cfg.getName() + ".js");
builder.append("'use strict';\n\n");
generateRequires(builder, ctx, cfg);
builder.append("/**\n* Kevoree component\n* @type {" + cfg.getName() + "}\n*/\n");
builder.append("var " + cfg.getName() + " = AbstractComponent.extend({\n");
builder.append("toString: '" + cfg.getName() + "',\n");
builder.append("tdef_version: " + AnnotatedElementHelper.annotationOrElse(cfg, "tdef_version", "1") + ",\n");
List<String> attributes = new ArrayList<String>();
builder.append("//Attributes\n");
for (Instance i : ConfigurationHelper.allInstances(cfg)) {
for (Property p : ThingHelper.allUsedProperties(i.getType())) {
if (p.isChangeable() && p.getCardinality() == null && AnnotatedElementHelper.isDefined(p.getType(), "java_primitive", "true") && p.eContainer() instanceof Thing) {
if (AnnotatedElementHelper.isDefined(p, "kevoree", "instance")) {
builder.append(getVariableName(i, p, ctx) + " : { \ndefaultValue: ");
final Expression e = ConfigurationHelper.initExpressions(cfg, i, p).get(0);
if (e != null) {
ctx.getCompiler().getThingActionCompiler().generate(e, builder, ctx);
} else {
builder.append("null\n");
}
builder.append("},\n");
} else if ((AnnotatedElementHelper.isDefined(p, "kevoree", "merge") || AnnotatedElementHelper.isDefined(p, "kevoree", "only")) && !attributes.contains(p.getName())) {
builder.append(getGlobalVariableName(p, ctx) + " : { \ndefaultValue: ");
final Expression e = ConfigurationHelper.initExpressions(cfg, i, p).get(0);
if (e != null) {
ctx.getCompiler().getThingActionCompiler().generate(e, builder, ctx);
} else {
builder.append("null\n");
}
builder.append("},\n");
attributes.add(p.getName());
}
}
}
}
builder.append("construct: function() {\n");
generateConstruct(builder, ctx, cfg);
builder.append("},\n\n");
builder.append("start: function (done) {\n");
generateStart(builder, ctx, cfg);
builder.append("},\n\n");
builder.append("stop: function (done) {\n");
generateStop(builder, ctx, cfg);
builder.append("}");
generatePorts(builder, ctx, cfg);
builder.append("});\n\n");
builder.append("module.exports = " + cfg.getName() + ";\n");
PrintWriter w = null;
try {
new File(ctx.getOutputDirectory() + "/lib").mkdirs();
w = new PrintWriter(new FileWriter(new File(ctx.getOutputDirectory() + "/lib/" + cfg.getName() + ".js")));
w.println(builder.toString());
w.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(w);
}
}
protected void generateKevoreeListener(StringBuilder builder, Context ctx, boolean isNumber, Property p, Instance i, boolean isGlobal, String accessor) {
//Update ThingML properties when Kevoree properties are updated
if (!isGlobal) //per instance mapping
builder.append("this.dictionary.on('" + i.getName() + "_" + ctx.getVariableName(p) + "', (newValue) => {");
else
builder.append("this.dictionary.on('" + ctx.getVariableName(p) + "', function (newValue) {");
if (isNumber) {
builder.append("newValue = Number(newValue);\n");
}
builder.append("this." + i.getName() + "." + ctx.getVariableName(p) + " = newValue;");
builder.append("});\n");
//Force update on startup, as listeners might be registered too late the first time
builder.append("this." + i.getName() + "." + ctx.getVariableName(p) + " = ");
if (!isGlobal) //per instance mapping
builder.append("this.dictionary." + accessor + "('" + i.getName() + "_" + ctx.getVariableName(p) + "')");
else
builder.append("this.dictionary." + accessor + "('" + ctx.getVariableName(p) + "')");
builder.append(";\n");
}
protected void generateThingMLListener(StringBuilder builder, Context ctx, Property p, Instance i, String accessor, boolean isGlobal) {
//Update Kevoree properties when ThingML properties are updated
String newValue = "newValue";
builder.append("this." + i.getName() + ".bus.on('" + p.getName() + "=', (newValue) => {");
if (!isGlobal)
builder.append("if(this.dictionary." + accessor + "('" + i.getName() + "_" + ctx.getVariableName(p) + "') !== " + newValue + ") {\n");
else
builder.append("if(this.dictionary." + accessor + "('" + ctx.getVariableName(p) + "') !== " + newValue + ") {\n");
if (!isGlobal)
builder.append("this.submitScript('set '+this.getNodeName()+'.'+this.getName()+'." + i.getName() + "_" + ctx.getVariableName(p) + " = \"'+" + newValue + "+'\"');\n");
else
builder.append("this.submitScript('set '+this.getNodeName()+'.'+this.getName()+'." + ctx.getVariableName(p) + " = \"'+" + newValue + "+'\"');\n");
builder.append("}");
builder.append("});\n");
}
protected String shortName(Instance i, Port p, Message m) {
String result = "";
if (i.getName().length() > 3) {
result += i.getName().substring(0, 3);
} else {
result += i.getName();
}
result += "_";
if (p.getName().length() > 3) {
result += p.getName().substring(0, 3);
} else {
result += p.getName();
}
result += "_";
if (m != null) {
result += m.getName();
}
return result;
}
}