groovy interpreter

This commit is contained in:
dlukyanov 2017-03-14 22:00:13 +02:00
parent 0cb0f36baf
commit fa779ea5df
11 changed files with 836 additions and 0 deletions

95
groovy/README.md Normal file
View file

@ -0,0 +1,95 @@
## Groovy Interpreter
### Samples
```groovy
%groovy
//get a parameter defined as z.angularBind('ngSearchParam', value, 'paragraph_id')
//g is a context object for groovy to avoid mix with z object
def param = g.angular('ngSearchParam')
//send request https://www.googleapis.com/customsearch/v1?q=ngSearchParam_value
def r = HTTP.get(
//assume you defined the groovy interpreter property
// `search_baseurl`='https://www.googleapis.com/customsearch/v1'
//in groovy object o.getProperty('A') == o.'A' == o.A == o['A']
url : g.search_baseurl,
query: [ q: param ],
headers: [
'Accept':'application/json',
//'Authorization:' : g.getProperty('search_auth'),
]
)
//check response code
if( r.response.code==200 ) {
g.html().with{
//g.html() renders %angular to output and returns groovy.xml.MarkupBuilder
h2("the response ${r.response.code}")
span( r.response.body )
h2("headers")
pre( r.response.headers.join('\n') )
}
} else {
//just to show that it's possible to use println with multiline groovy string to render output
println("""%angular
<script> alert ("code=${r.response.code} \n msg=${r.response.message}") </script>
""")
}
```
```groovy
%groovy
//renders a table with headers a, b, c and two rows
g.table(
[
['a','b','c'],
['a1','b1','c1'],
['a2','b2','c2'],
]
)
```
### the `g` object
* `g.angular(String name)`
Returns angular object by name. Look up notebook scope first and then global scope.
* `g.angularBind(String name, Object value)`
Assign a new `value` into angular object `name`
* `java.util.Properties g.getProperties()`
returns all properties defined for this interpreter
* `String g.getProperty('PROPERTY_NAME')`
```groovy
g.PROPERTY_NAME
g.'PROPERTY_NAME'
g['PROPERTY_NAME']
g.getProperties().getProperty('PROPERTY_NAME')
```
All above the accessor to named property defined in groovy interpreter.
In this case with name `PROPERTY_NAME`
* `groovy.xml.MarkupBuilder g.html()`
Starts or continues rendering of `%angular` to output and returns [groovy.xml.MarkupBuilder](https://www.google.com/search?q=groovy.xml.MarkupBuilder)
MarkupBuilder is usefull to generate html (xml)
* `void g.table(obj)`
starts or continues rendering table rows.
obj: List(rows) of List(columns) where first line is a header

4
groovy/README.txt Normal file
View file

@ -0,0 +1,4 @@
groovy language interpreter
project.name : ${project.name}
project.version : ${project.version}

152
groovy/pom.xml Normal file
View file

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>zeppelin</artifactId>
<groupId>org.apache.zeppelin</groupId>
<!--version>0.7</version-->
<version>0.8.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<groupId>org.apache.zeppelin</groupId>
<artifactId>zeppelin-groovy</artifactId>
<packaging>jar</packaging>
<version>0.8.0-SNAPSHOT</version>
<name>Zeppelin: Groovy interpreter</name>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>zeppelin-interpreter</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-all -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!--TODO: comment local `maven-checkstyle-plugin` and use zeppelin common check style-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
<executions>
</executions>
</plugin>
<plugin>
<artifactId>maven-enforcer-plugin</artifactId>
<version>1.3.1</version>
<executions>
<execution>
<id>enforce</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.8</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/../../interpreter/groovy</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
<includeScope>runtime</includeScope>
</configuration>
</execution>
<execution>
<id>copy-artifact</id>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/../../interpreter/groovy</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
<includeScope>runtime</includeScope>
<artifactItems>
<artifactItem>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
<type>${project.packaging}</type>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.5.3</version>
<configuration>
<descriptor>src/assembly/dep.xml</descriptor>
</configuration>
<executions>
<execution>
<id>create-archive</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,66 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You 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.
-->
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>bin</id>
<baseDirectory>groovy</baseDirectory>
<formats>
<format>zip</format>
</formats>
<fileSets>
<fileSet>
<directory>${project.basedir}</directory>
<outputDirectory>/</outputDirectory>
<filtered>true</filtered>
<includes>
<include>README*</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.build.directory}</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>*.jar</include>
<include>revision.txt</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.basedir}/src/main/groovy/</directory>
<outputDirectory>/classes/</outputDirectory>
<includes>
<include>*.groovy</include>
</includes>
</fileSet>
<!--fileSet>
<directory>${project.build.directory}/site</directory>
<outputDirectory>docs</outputDirectory>
</fileSet-->
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<unpack>false</unpack>
<scope>runtime</scope>
<!--excludes>
<exclude>junit:junit</exclude>
</excludes-->
</dependencySet>
</dependencySets>
</assembly>

View file

@ -0,0 +1 @@
to assemble groovy interpreter separately

View file

@ -0,0 +1,154 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
import groovy.json.JsonOutput
/**
* simple http rest client for groovy
* by dlukyanov@ukr.net
*/
@groovy.transform.CompileStatic
public class HTTP{
//default response handler
public static Closure TEXT_RECEIVER = {InputStream instr,Map ctx->
return instr.getText( (String)ctx.encoding );
}
public static Closure JSON_RECEIVER = { InputStream instr, Map ctx->
return new groovy.json.JsonSlurper().parse(instr,(String)ctx.encoding);
}
public static Closure FILE_RECEIVER(File f){
return { InputStream instr, Map ctx->
f<<instr;
return f;
}
}
public static Map<String,Object> get(Map<String,Object> ctx)throws IOException{
ctx.put('method','GET');
return send(ctx);
}
public static Map<String,Object> post(Map<String,Object> ctx)throws IOException{
ctx.put('method','POST');
return send(ctx);
}
public static Map<String,Object> put(Map<String,Object> ctx)throws IOException{
ctx.put('method','PUT');
return send(ctx);
}
public static Map<String,Object> delete(Map<String,Object> ctx)throws IOException{
ctx.put('method','DELETE');
return send(ctx);
}
public static Map<String,Object> send(Map<String,Object> ctx)throws IOException{
String url = ctx.url;
Map<String,String> headers = (Map<String,String>)ctx.headers;
String method = ctx.method;
Object body = ctx.body;
String encoding = ctx.encoding?:"UTF-8";
Closure receiver = (Closure)ctx.receiver;
Map<String,String> query = (Map<String,String>)ctx.query;
//copy context and set default values
ctx = [:] + ctx;
ctx.encoding = encoding;
String contentType="";
if(query){
url+="?"+query.collect{k,v-> k+"="+URLEncoder.encode(v,'UTF-8') }.join('&')
}
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setDoOutput(true);
connection.setRequestMethod(method);
if ( headers!=null && !headers.isEmpty() ) {
//add headers
for (Map.Entry<String, String> entry : headers.entrySet()) {
connection.addRequestProperty(entry.getKey(), entry.getValue());
if("content-type".equals(entry.getKey().toLowerCase()))contentType=entry.getValue();
}
}
if(body!=null){
//write body
OutputStream out = connection.getOutputStream();
if( body instanceof Closure ){
((Closure)body).call(out, ctx);
}else if(body instanceof InputStream){
out << (InputStream)body;
}else if(body instanceof Map){
if( contentType.matches("(?i)[^/]+/json") ){
out.withWriter((String)ctx.encoding){
it.append( JsonOutput.toJson((Map)body) );
it.flush();
}
}else{
throw new IOException("Map body type supported only for */json content-type");
}
}else if(body instanceof CharSequence){
out.withWriter((String)ctx.encoding){
it.append((CharSequence)body);
it.flush();
}
}else{
throw new IOException("Unsupported body type: "+body.getClass());
}
out.flush();
out.close();
out=null;
}
Map response = [:];
ctx.response = response;
response.code = connection.getResponseCode();
response.message = connection.getResponseMessage();
response.headers = connection.getHeaderFields();
InputStream instr = null;
if( ((int)response.code)>=400 ){
try{
instr = connection.getErrorStream();
}catch(Exception ei){}
}else{
try{
instr = connection.getInputStream();
}catch(java.io.IOException ei){
throw new IOException("fail to open InputStream for http code "+response.code+":"+ei);
}
}
if(instr!=null) {
instr = new BufferedInputStream(instr);
if(receiver==null){
if( response.headers['Content-Type']?.toString()?.indexOf('/json')>0 ){
receiver=JSON_RECEIVER;
} else receiver=TEXT_RECEIVER;
}
response.body = receiver(instr,ctx);
instr.close();
instr=null;
}
return ctx;
}
}

View file

@ -0,0 +1,2 @@
folder to place some groovy helpers to simplify custom code
as an example there is an HTTP.goovy simple class to call http/rest services

View file

@ -0,0 +1,168 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.zeppelin.groovy;
import java.io.StringWriter;
import org.slf4j.Logger;
import java.util.Properties;
import java.util.Collection;
import groovy.xml.MarkupBuilder;
import groovy.lang.Closure;
import org.apache.zeppelin.interpreter.InterpreterContext;
import org.apache.zeppelin.display.AngularObjectRegistry;
import org.apache.zeppelin.display.AngularObject;
/**
* Groovy interpreter for Zeppelin.
*/
public class GObject extends groovy.lang.GroovyObjectSupport {
Logger log;
StringWriter out;
Properties props;
InterpreterContext interpreterContext;
public GObject(Logger log, StringWriter out, Properties p, InterpreterContext ctx){
this.log=log;
this.out=out;
this.interpreterContext=ctx;
this.props=p;
}
public Object getProperty(String key){
if("log".equals(key))return log;
return props.getProperty(key);
}
public void setProperty(String key, Object value){
throw new RuntimeException("Set properties not supported: "+key+"="+value);
}
public Properties getProperties(){
return props;
}
private void startOutputType(String type){
StringBuffer sb=out.getBuffer();
if( sb.length()>0 ){
if( sb.length()<type.length() || !type.equals(sb.substring(0,type.length())) ){
log.error("try to start output `"+type+"` after non-"+type+" started");
}
}else{
out.append(type);
out.append('\n');
}
}
/**
* starts or continues rendering html/angular and returns MarkupBuilder to build html.
* <pre> g.html().with{
* h1("hello")
* h2("world")
* }</pre>
*/
public MarkupBuilder html(){
startOutputType("%angular");
return new MarkupBuilder(out);
}
/**
* starts or continues rendering table rows
* @param obj:
* 1. List(rows) of List(columns) where first line is a header
*/
public void table(Object obj){
if(obj==null)return;
StringBuffer sb=out.getBuffer();
startOutputType("%table");
if(obj instanceof groovy.lang.Closure){
//if closure run and get result collection
obj = ((Closure)obj).call();
}
if(obj instanceof Collection){
int count = 0;
for(Object row : ((Collection)obj)){
count++;
boolean rowStarted = false;
if(row instanceof Collection){
for( Object field: ((Collection)row) ){
if(rowStarted)sb.append('\t');
sb.append(field);
rowStarted=true;
}
}else{
sb.append(row);
}
sb.append('\n');
}
}else{
throw new RuntimeException("Not supported table value :"+obj.getClass());
}
}
private AngularObject getAngularObject(String name) {
AngularObjectRegistry registry = interpreterContext.getAngularObjectRegistry();
String noteId = interpreterContext.getNoteId();
// try get local object
AngularObject paragraphAo = registry.get(name, noteId, interpreterContext.getParagraphId());
AngularObject noteAo = registry.get(name, noteId, null);
AngularObject ao = paragraphAo != null ? paragraphAo : noteAo;
if (ao == null) {
// then global object
ao = registry.get(name, null, null);
}
return ao;
}
/**
* Get angular object. Look up notebook scope first and then global scope
* @param name variable name
* @return value
*/
public Object angular(String name) {
AngularObject ao = getAngularObject(name);
if (ao == null) {
return null;
} else {
return ao.get();
}
}
public void angularBind(String name, Object o, String noteId) {
AngularObjectRegistry registry = interpreterContext.getAngularObjectRegistry();
if (registry.get(name, noteId, null) == null) {
registry.add(name, o, noteId, null);
} else {
registry.get(name, noteId, null).set(o);
}
}
/**
* Create angular variable in notebook scope and bind with front end Angular display system.
* If variable exists, it'll be overwritten.
* @param name name of the variable
* @param o value
*/
public void angularBind(String name, Object o) {
angularBind(name, o, interpreterContext.getNoteId());
}
}

View file

@ -0,0 +1,184 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.zeppelin.groovy;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.io.File;
import java.util.*;
/*
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.Executor;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.lang3.StringUtils;
*/
import org.apache.zeppelin.interpreter.Interpreter;
import org.apache.zeppelin.interpreter.InterpreterContext;
import org.apache.zeppelin.interpreter.InterpreterPropertyBuilder;
import org.apache.zeppelin.interpreter.InterpreterResult;
import org.apache.zeppelin.interpreter.InterpreterResult.Code;
import org.apache.zeppelin.interpreter.InterpreterResult.Type;
import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion;
import org.apache.zeppelin.scheduler.Job;
import org.apache.zeppelin.scheduler.Scheduler;
import org.apache.zeppelin.scheduler.SchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.runtime.ResourceGroovyMethods;
import org.codehaus.groovy.runtime.StackTraceUtils;
/**
* Groovy interpreter for Zeppelin.
*/
public class GroovyInterpreter extends Interpreter {
Logger log = LoggerFactory.getLogger(GroovyInterpreter.class);
GroovyShell shell = null; //new GroovyShell();
public GroovyInterpreter(Properties property) {
super(property);
}
@Override
public void open() {
CompilerConfiguration conf = new CompilerConfiguration();
conf.setDebug(true);
shell = new GroovyShell(conf);
String classes = getProperty("GROOVY_CLASSES");
if(classes==null || classes.length()==0){
try {
File jar = new File(GroovyInterpreter.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath());
classes = new File(jar.getParentFile(),"classes").toString();
}catch(Exception e){}
}
log.info("groovy classes classpath: "+classes);
if(classes!=null && classes.length()>0){
shell.getClassLoader().addClasspath(classes);
}
}
@Override
public void close() {
shell = null;
}
@Override
public FormType getFormType() {
return FormType.NONE;
}
@Override
public int getProgress(InterpreterContext context) {
return 0;
}
@Override
public Scheduler getScheduler() {
return SchedulerFactory.singleton().createOrGetParallelScheduler(GroovyInterpreter.class.getName() + this.hashCode(), 10);
}
private Job getRunningJob(String paragraphId) {
Job foundJob = null;
Collection<Job> jobsRunning = getScheduler().getJobsRunning();
for (Job job : jobsRunning) {
if (job.getId().equals(paragraphId)) {
foundJob = job;
}
}
return foundJob;
}
@Override
public List<InterpreterCompletion> completion(String buf, int cursor) {
return null;
}
Map<String,Class<Script>> scriptCache = Collections.synchronizedMap( new WeakHashMap(1000) );
Script getGroovyScript(String id, String scriptText) /*throws SQLException*/ {
if(shell==null){
throw new RuntimeException("Groovy Shell is not initialized: null");
}
try{
Class<Script> clazz = scriptCache.get(scriptText);
if(clazz==null){
String scriptName=id+"_"+Long.toHexString(scriptText.hashCode())+".groovy";
clazz = (Class<Script>) shell.parse(scriptText, scriptName).getClass();
scriptCache.put(scriptText,clazz);
}
Script script=(Script)clazz.newInstance();
return script;
}catch(Throwable t){
throw new RuntimeException("Failed to parse groovy script: "+t,t);
}
}
@Override
public InterpreterResult interpret(String cmd, InterpreterContext contextInterpreter) {
try {
Script script = getGroovyScript(contextInterpreter.getParagraphId(), cmd);
Job runningJob = getRunningJob(contextInterpreter.getParagraphId());
runningJob.info().put("CURRENT_THREAD", Thread.currentThread()); //to be able to terminate thread
Map bindings = script.getBinding().getVariables();
bindings.clear();
StringWriter out = new StringWriter( (int) (cmd.length()*1.75) );
bindings.put("g", new GObject(log, out, this.getProperty(), contextInterpreter) );
bindings.put("out", new PrintWriter(out, true));
script.run();
bindings.clear();
InterpreterResult result = new InterpreterResult(Code.SUCCESS, out.toString());
log.info("RESULT: "+result);
return result;
}catch(Throwable t){
t = StackTraceUtils.deepSanitize(t);
String msg = t.toString()+"\n at "+t.getStackTrace()[0];
log.error("Failed to run script: "+t+"\n" + cmd+"\n", t);
return new InterpreterResult(Code.ERROR, msg);
}
}
@Override
public void cancel(InterpreterContext context) {
Job runningJob = getRunningJob(context.getParagraphId());
if (runningJob != null) {
Map<String, Object> info = runningJob.info();
Object object = info.get("CURRENT_THREAD");
if (object instanceof Thread) {
try {
Thread t = (Thread) object;
t.dumpStack();
t.stop();
}catch(Throwable t){
log.error("Failed to cancel script: "+t, t);
}
}
}
}
}

View file

@ -0,0 +1,9 @@
[
{
"group": "groovy",
"name": "groovy",
"className": "org.apache.zeppelin.groovy.GroovyInterpreter",
"properties": {
}
}
]

View file

@ -56,6 +56,7 @@
<module>zeppelin-zengine</module>
<module>zeppelin-display</module>
<module>spark-dependencies</module>
<module>groovy</module>
<module>spark</module>
<module>markdown</module>
<module>angular</module>