http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/ResourcePoolUtils.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/ResourcePoolUtils.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/ResourcePoolUtils.java index 1825bfe..9878d7e 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/ResourcePoolUtils.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/ResourcePoolUtils.java @@ -19,6 +19,7 @@ package org.apache.zeppelin.resource; import com.google.gson.Gson; import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterManagedProcess; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; import org.slf4j.Logger; @@ -134,3 +135,4 @@ public class ResourcePoolUtils { } } } +
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/WellKnownResourceName.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/WellKnownResourceName.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/WellKnownResourceName.java index 2d14fd4..4613c62 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/WellKnownResourceName.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/WellKnownResourceName.java @@ -20,7 +20,8 @@ package org.apache.zeppelin.resource; * Well known resource names in ResourcePool */ public enum WellKnownResourceName { - ParagraphResult("zeppelin.paragraph.result"); // paragraph run result + ZeppelinReplResult("zeppelin.repl.result"), // last object of repl + ZeppelinTableResult("zeppelin.paragraph.result.table"); // paragraph run result String name; WellKnownResourceName(String name) { http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/RemoteScheduler.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/RemoteScheduler.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/RemoteScheduler.java index 33a3ca6..28c7437 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/RemoteScheduler.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/RemoteScheduler.java @@ -20,6 +20,7 @@ package org.apache.zeppelin.scheduler; import org.apache.thrift.TException; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResult.Code; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterManagedProcess; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; import org.apache.zeppelin.scheduler.Job.Status; @@ -49,8 +50,8 @@ public class RemoteScheduler implements Scheduler { private RemoteInterpreterProcess interpreterProcess; public RemoteScheduler(String name, ExecutorService executor, String noteId, - RemoteInterpreterProcess interpreterProcess, SchedulerListener listener, - int maxConcurrency) { + RemoteInterpreterProcess interpreterProcess, SchedulerListener listener, + int maxConcurrency) { this.name = name; this.executor = executor; this.listener = listener; http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-interpreter/src/main/thrift/RemoteInterpreterService.thrift ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/thrift/RemoteInterpreterService.thrift b/zeppelin-interpreter/src/main/thrift/RemoteInterpreterService.thrift index 6c3fc36..32be4a4 100644 --- a/zeppelin-interpreter/src/main/thrift/RemoteInterpreterService.thrift +++ b/zeppelin-interpreter/src/main/thrift/RemoteInterpreterService.thrift @@ -48,7 +48,8 @@ enum RemoteInterpreterEventType { RESOURCE_GET = 7 OUTPUT_APPEND = 8, OUTPUT_UPDATE = 9, - ANGULAR_REGISTRY_PUSH=10 + ANGULAR_REGISTRY_PUSH = 10, + APP_STATUS_UPDATE = 11, } struct RemoteInterpreterEvent { @@ -56,6 +57,11 @@ struct RemoteInterpreterEvent { 2: string data // json serialized data } +struct RemoteApplicationResult { + 1: bool success, + 2: string msg +} + /* * The below variables(name, value) will be connected to getCompletions in paragraph.controller.js * @@ -99,4 +105,8 @@ service RemoteInterpreterService { void angularObjectAdd(1: string name, 2: string noteId, 3: string paragraphId, 4: string object); void angularObjectRemove(1: string name, 2: string noteId, 3: string paragraphId); void angularRegistryPush(1: string registry); + + RemoteApplicationResult loadApplication(1: string applicationInstanceId, 2: string packageInfo, 3: string noteId, 4: string paragraphId); + RemoteApplicationResult unloadApplication(1: string applicationInstanceId); + RemoteApplicationResult runApplication(1: string applicationInstanceId); } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/ApplicationLoaderTest.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/ApplicationLoaderTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/ApplicationLoaderTest.java new file mode 100644 index 0000000..06c4a6b --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/ApplicationLoaderTest.java @@ -0,0 +1,104 @@ +/* + * 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.helium; + +import org.apache.commons.io.FileUtils; +import org.apache.zeppelin.dep.DependencyResolver; +import org.apache.zeppelin.interpreter.InterpreterOutput; +import org.apache.zeppelin.interpreter.InterpreterOutputListener; +import org.apache.zeppelin.resource.LocalResourcePool; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.*; + +public class ApplicationLoaderTest { + private File tmpDir; + + @Before + public void setUp() { + tmpDir = new File(System.getProperty("java.io.tmpdir") + "/ZeppelinLTest_" + System.currentTimeMillis()); + tmpDir.mkdirs(); + } + + @After + public void tearDown() throws IOException { + FileUtils.deleteDirectory(tmpDir); + } + + @Test + public void loadUnloadApplication() throws Exception { + // given + LocalResourcePool resourcePool = new LocalResourcePool("pool1"); + DependencyResolver dep = new DependencyResolver(tmpDir.getAbsolutePath()); + ApplicationLoader appLoader = new ApplicationLoader(resourcePool, dep); + + HeliumPackage pkg1 = createPackageInfo(MockApplication1.class.getName(), "artifact1"); + ApplicationContext context1 = createContext("note1", "paragraph1", "app1"); + + // when load application + MockApplication1 app = (MockApplication1) ((ClassLoaderApplication) + appLoader.load(pkg1, context1)).getInnerApplication(); + + // then + assertFalse(app.isUnloaded()); + assertEquals(0, app.getNumRun()); + + // when unload + app.unload(); + + // then + assertTrue(app.isUnloaded()); + assertEquals(0, app.getNumRun()); + } + + public HeliumPackage createPackageInfo(String className, String artifact) { + HeliumPackage app1 = new HeliumPackage( + HeliumPackage.Type.APPLICATION, + "name1", + "desc1", + artifact, + className, + new String[][]{{}}); + return app1; + } + + public ApplicationContext createContext(String noteId, String paragraphId, String appInstanceId) { + ApplicationContext context1 = new ApplicationContext( + noteId, + paragraphId, + appInstanceId, + null, + new InterpreterOutput(new InterpreterOutputListener() { + @Override + public void onAppend(InterpreterOutput out, byte[] line) { + + } + + @Override + public void onUpdate(InterpreterOutput out, byte[] output) { + + } + })); + return context1; + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/MockApplication1.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/MockApplication1.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/MockApplication1.java new file mode 100644 index 0000000..df3afef --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/MockApplication1.java @@ -0,0 +1,52 @@ +/* + * 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.helium; + +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.resource.ResourceSet; + +/** + * Mock application + */ +public class MockApplication1 extends Application { + boolean unloaded; + int run; + + public MockApplication1(ApplicationContext context) { + super(context); + unloaded = false; + run = 0; + } + + @Override + public void run(ResourceSet args) { + run++; + } + + @Override + public void unload() { + unloaded = true; + } + + public boolean isUnloaded() { + return unloaded; + } + + public int getNumRun() { + return run; + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java index 7ffa170..5def888 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java @@ -73,6 +73,7 @@ public class RemoteAngularObjectTest implements AngularObjectRegistryListener { "fakeRepo", env, 10 * 1000, + null, null ); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java index 4a473f3..74649b1 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java @@ -70,7 +70,8 @@ public class RemoteInterpreterOutputTestStream implements RemoteInterpreterProce "fakeRepo", env, 10 * 1000, - this); + this, + null); intpGroup.get("note").add(intp); intp.setInterpreterGroup(intpGroup); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java index f9d7d39..0158282 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java @@ -41,9 +41,9 @@ public class RemoteInterpreterProcessTest { @Test public void testStartStop() { InterpreterGroup intpGroup = new InterpreterGroup(); - RemoteInterpreterProcess rip = new RemoteInterpreterProcess( + RemoteInterpreterManagedProcess rip = new RemoteInterpreterManagedProcess( INTERPRETER_SCRIPT, "nonexists", "fakeRepo", new HashMap<String, String>(), - 10 * 1000, null); + 10 * 1000, null, null); assertFalse(rip.isRunning()); assertEquals(0, rip.referenceCount()); assertEquals(1, rip.reference(intpGroup)); @@ -58,7 +58,7 @@ public class RemoteInterpreterProcessTest { @Test public void testClientFactory() throws Exception { InterpreterGroup intpGroup = new InterpreterGroup(); - RemoteInterpreterProcess rip = new RemoteInterpreterProcess( + RemoteInterpreterManagedProcess rip = new RemoteInterpreterManagedProcess( INTERPRETER_SCRIPT, "nonexists", "fakeRepo", new HashMap<String, String>(), mock(RemoteInterpreterEventPoller.class), 10 * 1000); rip.reference(intpGroup); @@ -96,8 +96,14 @@ public class RemoteInterpreterProcessTest { InterpreterGroup intpGroup = mock(InterpreterGroup.class); when(intpGroup.getProperty()).thenReturn(properties); when(intpGroup.containsKey(Constants.EXISTING_PROCESS)).thenReturn(true); - RemoteInterpreterProcess rip = new RemoteInterpreterProcess(INTERPRETER_SCRIPT, "nonexists", - "fakeRepo", new HashMap<String, String>(), 10 * 1000, null); + + RemoteInterpreterProcess rip = new RemoteInterpreterManagedProcess( + INTERPRETER_SCRIPT, + "nonexists", + "fakeRepo", + new HashMap<String, String>(), + mock(RemoteInterpreterEventPoller.class) + , 10 * 1000); assertFalse(rip.isRunning()); assertEquals(0, rip.referenceCount()); assertEquals(1, rip.reference(intpGroup)); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java index ea0bbf4..af1c447 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java @@ -89,6 +89,7 @@ public class RemoteInterpreterTest { "fakeRepo", env, 10 * 1000, + null, null); } @@ -106,6 +107,7 @@ public class RemoteInterpreterTest { "fakeRepo", env, 10 * 1000, + null, null); } @@ -204,6 +206,7 @@ public class RemoteInterpreterTest { "fakeRepo", env, 10 * 1000, + null, null); @@ -219,6 +222,7 @@ public class RemoteInterpreterTest { "fakeRepo", env, 10 * 1000, + null, null); intpGroup.get("note").add(intpB); @@ -683,7 +687,7 @@ public class RemoteInterpreterTest { //Given final Client client = Mockito.mock(Client.class); final RemoteInterpreter intr = new RemoteInterpreter(new Properties(), "noteId", - MockInterpreterA.class.getName(), "runner", "path","localRepo", env, 10 * 1000, null); + MockInterpreterA.class.getName(), "runner", "path","localRepo", env, 10 * 1000, null, null); final AngularObjectRegistry registry = new AngularObjectRegistry("spark", null); registry.add("name", "DuyHai DOAN", "nodeId", "paragraphId"); final InterpreterGroup interpreterGroup = new InterpreterGroup("groupId"); @@ -723,11 +727,12 @@ public class RemoteInterpreterTest { p, "note", MockInterpreterEnv.class.getName(), - new File("../bin/interpreter.sh").getAbsolutePath(), + new File(INTERPRETER_SCRIPT).getAbsolutePath(), "fake", "fakeRepo", env, 10 * 1000, + null, null); intpGroup.put("note", new LinkedList<Interpreter>()); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-interpreter/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java index ae5e6f5..02dba20 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java @@ -69,6 +69,7 @@ public class DistributedResourcePoolTest { "fakeRepo", env, 10 * 1000, + null, null ); @@ -86,6 +87,7 @@ public class DistributedResourcePoolTest { "fakeRepo", env, 10 * 1000, + null, null ); @@ -110,11 +112,11 @@ public class DistributedResourcePoolTest { intp1.open(); intp2.open(); - eventPoller1 = new RemoteInterpreterEventPoller(null); + eventPoller1 = new RemoteInterpreterEventPoller(null, null); eventPoller1.setInterpreterGroup(intpGroup1); eventPoller1.setInterpreterProcess(intpGroup1.getRemoteInterpreterProcess()); - eventPoller2 = new RemoteInterpreterEventPoller(null); + eventPoller2 = new RemoteInterpreterEventPoller(null, null); eventPoller2.setInterpreterGroup(intpGroup2); eventPoller2.setInterpreterProcess(intpGroup2.getRemoteInterpreterProcess()); @@ -140,13 +142,12 @@ public class DistributedResourcePoolTest { InterpreterResult ret; intp1.interpret("put key1 value1", context); intp2.interpret("put key2 value2", context); - int numInterpreterResult = 2; ret = intp1.interpret("getAll", context); - assertEquals(numInterpreterResult + 2, gson.fromJson(ret.message(), ResourceSet.class).size()); + assertEquals(2, gson.fromJson(ret.message(), ResourceSet.class).size()); ret = intp2.interpret("getAll", context); - assertEquals(numInterpreterResult + 2, gson.fromJson(ret.message(), ResourceSet.class).size()); + assertEquals(2, gson.fromJson(ret.message(), ResourceSet.class).size()); ret = intp1.interpret("get key1", context); assertEquals("value1", gson.fromJson(ret.message(), String.class)); @@ -218,16 +219,15 @@ public class DistributedResourcePoolTest { intp2.interpret("put note2:paragraph1:key1 value1", context); intp2.interpret("put note2:paragraph2:key2 value2", context); - int numInterpreterResult = 2; // then get all resources. - assertEquals(numInterpreterResult + 4, ResourcePoolUtils.getAllResources().size()); + assertEquals(4, ResourcePoolUtils.getAllResources().size()); // when remove all resources from note1 ResourcePoolUtils.removeResourcesBelongsToNote("note1"); // then resources should be removed. - assertEquals(numInterpreterResult + 2, ResourcePoolUtils.getAllResources().size()); + assertEquals(2, ResourcePoolUtils.getAllResources().size()); assertEquals("", gson.fromJson( intp1.interpret("get note1:paragraph1:key1", context).message(), String.class)); @@ -240,7 +240,7 @@ public class DistributedResourcePoolTest { ResourcePoolUtils.removeResourcesBelongsToParagraph("note2", "paragraph1"); // then 1 - assertEquals(numInterpreterResult + 1, ResourcePoolUtils.getAllResources().size()); + assertEquals(1, ResourcePoolUtils.getAllResources().size()); assertEquals("value2", gson.fromJson( intp1.interpret("get note2:paragraph2:key2", context).message(), String.class)); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java index 40dcef2..f17d88d 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java @@ -80,7 +80,8 @@ public class RemoteSchedulerTest implements RemoteInterpreterProcessListener { "fakeRepo", env, 10 * 1000, - this); + this, + null); intpGroup.put("note", new LinkedList<Interpreter>()); intpGroup.get("note").add(intpA); @@ -168,7 +169,8 @@ public class RemoteSchedulerTest implements RemoteInterpreterProcessListener { "fakeRepo", env, 10 * 1000, - this); + this, + null); intpGroup.put("note", new LinkedList<Interpreter>()); intpGroup.get("note").add(intpA); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-server/src/main/java/org/apache/zeppelin/rest/HeliumRestApi.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/HeliumRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/HeliumRestApi.java new file mode 100644 index 0000000..062f5b9 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/HeliumRestApi.java @@ -0,0 +1,108 @@ +/* + * 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.rest; + +import com.google.gson.Gson; +import org.apache.zeppelin.helium.Helium; +import org.apache.zeppelin.helium.HeliumApplicationFactory; +import org.apache.zeppelin.helium.HeliumPackage; +import org.apache.zeppelin.notebook.Note; +import org.apache.zeppelin.notebook.Notebook; +import org.apache.zeppelin.notebook.Paragraph; +import org.apache.zeppelin.server.JsonResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.*; +import javax.ws.rs.core.Response; + +/** + * Helium Rest Api + */ +@Path("/helium") +@Produces("application/json") +public class HeliumRestApi { + Logger logger = LoggerFactory.getLogger(HeliumRestApi.class); + + private Helium helium; + private HeliumApplicationFactory applicationFactory; + private Notebook notebook; + private Gson gson = new Gson(); + + public HeliumRestApi() { + } + + public HeliumRestApi(Helium helium, + HeliumApplicationFactory heliumApplicationFactory, + Notebook notebook) { + this.helium = helium; + this.applicationFactory = heliumApplicationFactory; + this.notebook = notebook; + } + + /** + * Get all packages + * @return + */ + @GET + @Path("all") + public Response getAll() { + return new JsonResponse(Response.Status.OK, "", helium.getAllPackageInfo()).build(); + } + + @GET + @Path("suggest/{noteId}/{paragraphId}") + public Response suggest(@PathParam("noteId") String noteId, + @PathParam("paragraphId") String paragraphId) { + Note note = notebook.getNote(noteId); + if (note == null) { + return new JsonResponse(Response.Status.NOT_FOUND, "Note " + noteId + " not found").build(); + } + + Paragraph paragraph = note.getParagraph(paragraphId); + if (paragraph == null) { + return new JsonResponse(Response.Status.NOT_FOUND, "Paragraph " + paragraphId + " not found") + .build(); + } + + return new JsonResponse(Response.Status.OK, "", helium.suggestApp(paragraph)).build(); + } + + @POST + @Path("load/{noteId}/{paragraphId}") + public Response suggest(@PathParam("noteId") String noteId, + @PathParam("paragraphId") String paragraphId, + String heliumPackage) { + + Note note = notebook.getNote(noteId); + if (note == null) { + return new JsonResponse(Response.Status.NOT_FOUND, "Note " + noteId + " not found").build(); + } + + Paragraph paragraph = note.getParagraph(paragraphId); + if (paragraph == null) { + return new JsonResponse(Response.Status.NOT_FOUND, "Paragraph " + paragraphId + " not found") + .build(); + } + HeliumPackage pkg = gson.fromJson(heliumPackage, HeliumPackage.class); + + String appId = applicationFactory.loadAndRun(pkg, paragraph); + return new JsonResponse(Response.Status.OK, "", appId).build(); + } + +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java index 0ff0dc6..0f7d8a1 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java @@ -17,10 +17,23 @@ package org.apache.zeppelin.server; +import java.io.File; +import java.io.IOException; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutorService; + +import javax.net.ssl.SSLContext; +import javax.servlet.DispatcherType; +import javax.ws.rs.core.Application; + import org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; import org.apache.zeppelin.dep.DependencyResolver; +import org.apache.zeppelin.helium.Helium; +import org.apache.zeppelin.helium.HeliumApplicationFactory; import org.apache.zeppelin.interpreter.InterpreterFactory; import org.apache.zeppelin.notebook.Notebook; import org.apache.zeppelin.notebook.NotebookAuthorization; @@ -46,14 +59,6 @@ import org.eclipse.jetty.webapp.WebAppContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.servlet.DispatcherType; -import javax.ws.rs.core.Application; -import java.io.File; -import java.io.IOException; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.Set; - /** * Main class of Zeppelin. */ @@ -63,6 +68,8 @@ public class ZeppelinServer extends Application { public static Notebook notebook; public static Server jettyWebServer; public static NotebookServer notebookWsServer; + public static Helium helium; + public static HeliumApplicationFactory heliumApplicationFactory; private SchedulerFactory schedulerFactory; private InterpreterFactory replFactory; @@ -77,9 +84,12 @@ public class ZeppelinServer extends Application { this.depResolver = new DependencyResolver( conf.getString(ConfVars.ZEPPELIN_INTERPRETER_LOCALREPO)); + + this.helium = new Helium(conf.getHeliumConfPath(), conf.getHeliumDefaultLocalRegistryPath()); + this.heliumApplicationFactory = new HeliumApplicationFactory(); this.schedulerFactory = new SchedulerFactory(); this.replFactory = new InterpreterFactory(conf, notebookWsServer, - notebookWsServer, depResolver); + notebookWsServer, heliumApplicationFactory, depResolver); this.notebookRepo = new NotebookRepoSync(conf); this.notebookIndex = new LuceneSearch(); this.notebookAuthorization = new NotebookAuthorization(conf); @@ -87,6 +97,13 @@ public class ZeppelinServer extends Application { notebook = new Notebook(conf, notebookRepo, schedulerFactory, replFactory, notebookWsServer, notebookIndex, notebookAuthorization, credentials); + + // to update notebook from application event from remote process. + heliumApplicationFactory.setNotebook(notebook); + // to update fire websocket event on application event. + heliumApplicationFactory.setApplicationEventListener(notebookWsServer); + + notebook.addNotebookEventListener(heliumApplicationFactory); } public static void main(String[] args) throws InterruptedException { @@ -294,6 +311,9 @@ public class ZeppelinServer extends Application { NotebookRestApi notebookApi = new NotebookRestApi(notebook, notebookWsServer, notebookIndex); singletons.add(notebookApi); + HeliumRestApi heliumApi = new HeliumRestApi(helium, heliumApplicationFactory, notebook); + singletons.add(heliumApi); + InterpreterRestApi interpreterApi = new InterpreterRestApi(replFactory); singletons.add(interpreterApi); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java index 1c3220b..4261a65 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java @@ -34,6 +34,8 @@ import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.AngularObjectRegistryListener; +import org.apache.zeppelin.helium.ApplicationEventListener; +import org.apache.zeppelin.helium.HeliumPackage; import org.apache.zeppelin.interpreter.InterpreterGroup; import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; @@ -61,7 +63,7 @@ import org.slf4j.LoggerFactory; */ public class NotebookServer extends WebSocketServlet implements NotebookSocketListener, JobListenerFactory, AngularObjectRegistryListener, - RemoteInterpreterProcessListener { + RemoteInterpreterProcessListener, ApplicationEventListener { /** * Job manager service type */ @@ -759,6 +761,7 @@ public class NotebookServer extends WebSocketServlet implements if (interpreterGroupId.equals(setting.getInterpreterGroup(note.id()).getId())) { AngularObjectRegistry angularObjectRegistry = setting .getInterpreterGroup(note.id()).getAngularObjectRegistry(); + // first trying to get local registry ao = angularObjectRegistry.get(varName, noteId, paragraphId); if (ao == null) { @@ -1136,7 +1139,6 @@ public class NotebookServer extends WebSocketServlet implements .put("noteId", noteId) .put("paragraphId", paragraphId) .put("data", output); - Paragraph paragraph = notebook().getNote(noteId).getParagraph(paragraphId); broadcast(noteId, msg); } @@ -1152,7 +1154,60 @@ public class NotebookServer extends WebSocketServlet implements .put("noteId", noteId) .put("paragraphId", paragraphId) .put("data", output); - Paragraph paragraph = notebook().getNote(noteId).getParagraph(paragraphId); + broadcast(noteId, msg); + } + + /** + * When application append output + * @param noteId + * @param paragraphId + * @param appId + * @param output + */ + @Override + public void onOutputAppend(String noteId, String paragraphId, String appId, String output) { + Message msg = new Message(OP.APP_APPEND_OUTPUT) + .put("noteId", noteId) + .put("paragraphId", paragraphId) + .put("appId", appId) + .put("data", output); + broadcast(noteId, msg); + } + + /** + * When application update output + * @param noteId + * @param paragraphId + * @param appId + * @param output + */ + @Override + public void onOutputUpdated(String noteId, String paragraphId, String appId, String output) { + Message msg = new Message(OP.APP_UPDATE_OUTPUT) + .put("noteId", noteId) + .put("paragraphId", paragraphId) + .put("appId", appId) + .put("data", output); + broadcast(noteId, msg); + } + + @Override + public void onLoad(String noteId, String paragraphId, String appId, HeliumPackage pkg) { + Message msg = new Message(OP.APP_LOAD) + .put("noteId", noteId) + .put("paragraphId", paragraphId) + .put("appId", appId) + .put("pkg", pkg); + broadcast(noteId, msg); + } + + @Override + public void onStatusChange(String noteId, String paragraphId, String appId, String status) { + Message msg = new Message(OP.APP_STATUS_CHANGE) + .put("noteId", noteId) + .put("paragraphId", paragraphId) + .put("appId", appId) + .put("status", status); broadcast(noteId, msg); } @@ -1282,19 +1337,17 @@ public class NotebookServer extends WebSocketServlet implements List<InterpreterSetting> intpSettings = notebook.getInterpreterFactory() .getInterpreterSettings(note.getId()); - if (intpSettings.isEmpty()) + if (intpSettings.isEmpty()) { continue; - for (InterpreterSetting setting : intpSettings) { - if (setting.getInterpreterGroup(note.id()).getId().equals(interpreterGroupId)) { - broadcast( - note.id(), - new Message(OP.ANGULAR_OBJECT_UPDATE) - .put("angularObject", object) - .put("interpreterGroupId", interpreterGroupId) - .put("noteId", note.id()) - .put("paragraphId", object.getParagraphId())); - } } + + broadcast( + note.id(), + new Message(OP.ANGULAR_OBJECT_UPDATE) + .put("angularObject", object) + .put("interpreterGroupId", interpreterGroupId) + .put("noteId", note.id()) + .put("paragraphId", object.getParagraphId())); } } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java index 81c7190..0ff0135 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java @@ -165,7 +165,7 @@ public class SparkParagraphIT extends AbstractZeppelinIT { } WebElement paragraph1Result = driver.findElement(By.xpath( - getParagraphXPath(1) + "//div[@class=\"tableDisplay\"]//table")); + getParagraphXPath(1) + "//div[@class=\"tableDisplay\"]/div/div/div/div/div/div[1]")); collector.checkThat("Paragraph from SparkParagraphIT of testSqlSpark result: ", paragraph1Result.getText().toString(), CoreMatchers.equalTo("age\njob\nmarital\neducation\nbalance\n" + "30 unemployed married primary 1,787")); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AbstractTestRestApi.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AbstractTestRestApi.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AbstractTestRestApi.java index ada2ef8..f2d4c99 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AbstractTestRestApi.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AbstractTestRestApi.java @@ -251,7 +251,7 @@ public abstract class AbstractTestRestApi { request = httpGet("/"); isRunning = request.getStatusCode() == 200; } catch (IOException e) { - LOG.error("Exception in AbstractTestRestApi while checkIfServerIsRunning ", e); + LOG.error("AbstractTestRestApi.checkIfServerIsRunning() fails .. ZeppelinServer is not running"); isRunning = false; } finally { if (request != null) { http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java index d234ffd..3c77b45 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java @@ -24,7 +24,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.commons.exec.CommandLine; +import org.apache.commons.exec.DefaultExecutor; +import org.apache.commons.exec.ExecuteWatchdog; +import org.apache.commons.exec.PumpStreamHandler; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.zeppelin.interpreter.InterpreterSetting; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.Paragraph; @@ -101,6 +106,7 @@ public class ZeppelinSparkClusterTest extends AbstractTestRestApi { ); note.run(p.getId()); waitForFinish(p); + System.err.println("sparkRTest=" + p.getResult().message()); assertEquals(Status.FINISHED, p.getStatus()); assertEquals("[1] 3", p.getResult().message()); } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-web/src/app/notebook/paragraph/paragraph-chart-selector.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-chart-selector.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-chart-selector.html index 76135b1..507c57f 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph-chart-selector.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph-chart-selector.html @@ -12,56 +12,112 @@ See the License for the specific language governing permissions and limitations under the License. --> -<div ng-if="paragraph.result.type == 'TABLE' && !asIframe && !viewOnly" +<div id="{{paragraph.id}}_switch" + ng-if="(paragraph.result.type == 'TABLE' || apps.length > 0 || suggestion.available && suggestion.available.length > 0) && !asIframe && !viewOnly" + class="btn-group" style='margin-bottom: 10px;'> - <div id="{{paragraph.id}}_switch" - class="btn-group"> - <button type="button" class="btn btn-default btn-sm" - ng-class="{'active': isGraphMode('table')}" - ng-click="setGraphMode('table', true)" ><i class="fa fa-table"></i> - </button> - <button type="button" class="btn btn-default btn-sm" - ng-class="{'active': isGraphMode('multiBarChart')}" - ng-click="setGraphMode('multiBarChart', true)"><i class="fa fa-bar-chart"></i> - </button> - <button type="button" class="btn btn-default btn-sm" - ng-class="{'active': isGraphMode('pieChart')}" - ng-click="setGraphMode('pieChart', true)"><i class="fa fa-pie-chart"></i> - </button> - <button type="button" class="btn btn-default btn-sm" - ng-class="{'active': isGraphMode('stackedAreaChart')}" - ng-click="setGraphMode('stackedAreaChart', true)"><i class="fa fa-area-chart"></i> - </button> - <button type="button" class="btn btn-default btn-sm" - ng-class="{'active': isGraphMode('lineChart') || isGraphMode('lineWithFocusChart')}" - ng-click="paragraph.config.graph.lineWithFocus ? setGraphMode('lineWithFocusChart', true) : setGraphMode('lineChart', true)"><i class="fa fa-line-chart"></i> - </button> - <button type="button" class="btn btn-default btn-sm" - ng-class="{'active': isGraphMode('scatterChart')}" - ng-click="setGraphMode('scatterChart', true)"><i class="cf cf-scatter-chart"></i> - </button> - </div> - <span class="btn-group"> - <button type="button" class="btn btn-default btn-sm" - style="margin-left:10px" - ng-click="exportToDSV(',')" - tooltip="Download Data as CSV" tooltip-placement="bottom"> - <i class="fa fa-download"></i> - </button> - <button type="button" class="btn btn-default btn-sm dropdown-toggle caretBtn" - data-toggle="dropdown"> - <span class="caret" style="margin: 0px;"></span> - <span class="sr-only">Toggle Dropdown</span> - </button> - <ul class="dropdown-menu" role="menu" style="min-width: 70px;"> - <li ng-click="exportToDSV(',')"><a>CSV</a></li> - <li ng-click="exportToDSV('\t')"><a>TSV</a></li> - </ul> - </span> - <span ng-if="getGraphMode()!='table'" - style="margin-left:5px; cursor:pointer; display: inline-block; vertical-align:top; position: relative; line-height:30px;"> - <a class="btnText" ng-click="toggleGraphOption()"> - settings <span ng-class="paragraph.config.graph.optionOpen ? 'fa fa-caret-up' : 'fa fa-caret-down'"></span> - </a> - </span> + + <button type="button" class="btn btn-default btn-sm" + ng-if="paragraph.result.type == 'TABLE'" + ng-class="{'active': isGraphMode('table')}" + ng-click="setGraphMode('table', true)" ><i class="fa fa-table"></i> + </button> + <button type="button" class="btn btn-default btn-sm" + ng-if="paragraph.result.type == 'TABLE'" + ng-class="{'active': isGraphMode('multiBarChart')}" + ng-click="setGraphMode('multiBarChart', true)"><i class="fa fa-bar-chart"></i> + </button> + <button type="button" class="btn btn-default btn-sm" + ng-if="paragraph.result.type == 'TABLE'" + ng-class="{'active': isGraphMode('pieChart')}" + ng-click="setGraphMode('pieChart', true)"><i class="fa fa-pie-chart"></i> + </button> + <button type="button" class="btn btn-default btn-sm" + ng-if="paragraph.result.type == 'TABLE'" + ng-class="{'active': isGraphMode('stackedAreaChart')}" + ng-click="setGraphMode('stackedAreaChart', true)"><i class="fa fa-area-chart"></i> + </button> + <button type="button" class="btn btn-default btn-sm" + ng-if="paragraph.result.type == 'TABLE'" + ng-class="{'active': isGraphMode('lineChart') || isGraphMode('lineWithFocusChart')}" + ng-click="paragraph.config.graph.lineWithFocus ? setGraphMode('lineWithFocusChart', true) : setGraphMode('lineChart', true)"><i class="fa fa-line-chart"></i> + </button> + <button type="button" class="btn btn-default btn-sm" + ng-if="paragraph.result.type == 'TABLE'" + ng-class="{'active': isGraphMode('scatterChart')}" + ng-click="setGraphMode('scatterChart', true)"><i class="cf cf-scatter-chart"></i> + </button> + + <button type="button" + ng-if="paragraph.result.type != 'TABLE'" + ng-click="switchApp()" + ng-class="{'active' : !paragraph.config.helium.activeApp}" + class="btn btn-default btn-sm"><i class="fa fa-terminal"></i> + </button> + + <button type="button" + class="btn btn-default btn-sm" + ng-repeat="app in apps" + ng-click="switchApp(app.id)" + ng-class="{'active' : app.id == paragraph.config.helium.activeApp}" + ng-bind-html="app.pkg.icon"> + </button> +</div> + +<div id="{{paragraph.id}}_helium" + ng-if="(suggestion.available && suggestion.available.length > 0) && !asIframe && !viewOnly" + class="btn-group" + style='margin-bottom: 10px;'> + <button type="button" + class="btn btn-default btn-sm dropdown-toggle" + ng-if="suggestion.available && suggestion.available.length > 0" + data-toggle="dropdown" + style="font-weight:bold; background-color:#ffdf96; border: 1px solid #FED233"> + He + </button> + <ul class="dropdown-menu" + style="z-index:1002" + ng-if="suggestion.available && suggestion.available.length > 0" + role="menu"> + <li class="appSuggestion"> + <div ng-repeat="pkgInfo in suggestion.available" + style="margin-bottom:5px"> + <button type="button" + class="btn btn-default btn-sm" + ng-click="loadApp(pkgInfo.pkg)" + ng-bind-html="pkgInfo.pkg.icon"> + </button> + <span class="inline">{{pkgInfo.pkg.name}}</span> + </div> + </li> + </ul> </div> + +<div class="btn-group" + ng-if="paragraph.result.type == 'TABLE' && !asIframe && !viewOnly" + style="margin-bottom: 10px;"> + <button type="button" class="btn btn-default btn-sm" + style="margin-left:10px" + ng-click="exportToDSV(',')" + tooltip="Download Data as CSV" tooltip-placement="bottom"> + <i class="fa fa-download"></i> + </button> + <button type="button" class="btn btn-default btn-sm dropdown-toggle caretBtn" + data-toggle="dropdown"> + <span class="caret" style="margin: 0px;"></span> + <span class="sr-only">Toggle Dropdown</span> + </button> + <ul class="dropdown-menu" role="menu" style="min-width: 70px;"> + <li ng-click="exportToDSV(',')"><a>CSV</a></li> + <li ng-click="exportToDSV('\t')"><a>TSV</a></li> + </ul> +</div> + +<span + ng-if="getResultType()=='TABLE' && !paragraph.config.helium.activeApp && getGraphMode()!='table' && !asIframe && !viewOnly" + style="margin-left:10px; cursor:pointer; display: inline-block; vertical-align:top; position: relative; line-height:30px;"> + <a class="btnText" ng-click="toggleGraphOption()"> + settings <span ng-class="paragraph.config.graph.optionOpen ? 'fa fa-caret-up' : 'fa fa-caret-down'"></span> + </a> +</span> + http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-web/src/app/notebook/paragraph/paragraph-results.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-results.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-results.html index 612fdbd..fd608c5 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph-results.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph-results.html @@ -13,6 +13,7 @@ limitations under the License. --> <div id="p{{paragraph.id}}_resize" + ng-if="!paragraph.config.helium.activeApp" style='padding-bottom: 5px;' resize='{"allowresize": "{{!asIframe && !viewOnly}}", "graphType": "{{getResultType()}}"}' resizable on-resize="resizeParagraph(width, height);"> @@ -60,3 +61,11 @@ limitations under the License. ng-bind="paragraph.errorMessage"> </div> </div> + +<div ng-repeat="app in apps"> + <div id="p{{app.id}}" + ng-show="paragraph.config.helium.activeApp == app.id"> + </div> +</div> + + http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 398191c..211ab59 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -16,7 +16,7 @@ angular.module('zeppelinWebApp') .controller('ParagraphCtrl', function($scope,$rootScope, $route, $window, $element, $routeParams, $location, - $timeout, $compile, websocketMsgSrv, ngToast, SaveAsService) { + $timeout, $compile, $http, websocketMsgSrv, baseUrlSrv, ngToast, SaveAsService) { var ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_'; $scope.parentNote = null; $scope.paragraph = null; @@ -116,10 +116,21 @@ angular.module('zeppelinWebApp') } else if ($scope.getResultType() === 'TEXT') { $scope.renderText(); } + + getApplicationStates(); + getSuggestions(); + + var activeApp = _.get($scope.paragraph.config, 'helium.activeApp'); + if (activeApp) { + var app = _.find($scope.apps, {id: activeApp}); + renderApp(app); + } }; - $scope.renderHtml = function() { - var retryRenderer = function() { + + + $scope.renderHtml = function() { + var retryRenderer = function() { if (angular.element('#p' + $scope.paragraph.id + '_html').length) { try { angular.element('#p' + $scope.paragraph.id + '_html').html($scope.paragraph.result.msg); @@ -202,8 +213,23 @@ angular.module('zeppelinWebApp') $scope.$on('angularObjectUpdate', function(event, data) { var noteId = $route.current.pathParams.noteId; - if (!data.noteId || (data.noteId === noteId && (!data.paragraphId || data.paragraphId === $scope.paragraph.id))) { - var scope = paragraphScope; + if (!data.noteId || data.noteId === noteId) { + var scope; + var registry; + + if (!data.paragraphId || data.paragraphId === $scope.paragraph.id) { + scope = paragraphScope; + registry = angularObjectRegistry; + } else { + var app = _.find($scope.apps, { id: data.paragraphId}); + if (app) { + scope = getAppScope(app); + registry = getAppRegistry(app); + } else { + // no matching app in this paragraph + return; + } + } var varName = data.angularObject.name; if (angular.equals(data.angularObject.object, scope[varName])) { @@ -211,32 +237,32 @@ angular.module('zeppelinWebApp') return; } - if (!angularObjectRegistry[varName]) { - angularObjectRegistry[varName] = { + if (!registry[varName]) { + registry[varName] = { interpreterGroupId : data.interpreterGroupId, noteId : data.noteId, paragraphId : data.paragraphId }; } else { - angularObjectRegistry[varName].noteId = angularObjectRegistry[varName].noteId || data.noteId; - angularObjectRegistry[varName].paragraphId = angularObjectRegistry[varName].paragraphId || data.paragraphId; + registry[varName].noteId = registry[varName].noteId || data.noteId; + registry[varName].paragraphId = registry[varName].paragraphId || data.paragraphId; } - angularObjectRegistry[varName].skipEmit = true; + registry[varName].skipEmit = true; - if (!angularObjectRegistry[varName].clearWatcher) { - angularObjectRegistry[varName].clearWatcher = scope.$watch(varName, function(newValue, oldValue) { - console.log('angular object (paragraph) updated %o %o', varName, angularObjectRegistry[varName]); - if (angularObjectRegistry[varName].skipEmit) { - angularObjectRegistry[varName].skipEmit = false; + if (!registry[varName].clearWatcher) { + registry[varName].clearWatcher = scope.$watch(varName, function(newValue, oldValue) { + console.log('angular object (paragraph) updated %o %o', varName, registry[varName]); + if (registry[varName].skipEmit) { + registry[varName].skipEmit = false; return; } websocketMsgSrv.updateAngularObject( - angularObjectRegistry[varName].noteId, - angularObjectRegistry[varName].paragraphId, + registry[varName].noteId, + registry[varName].paragraphId, varName, newValue, - angularObjectRegistry[varName].interpreterGroupId); + registry[varName].interpreterGroupId); }); } console.log('angular object (paragraph) created %o', varName); @@ -258,14 +284,30 @@ angular.module('zeppelinWebApp') $scope.$on('angularObjectRemove', function(event, data) { var noteId = $route.current.pathParams.noteId; - if (!data.noteId || (data.noteId === noteId && (!data.paragraphId || data.paragraphId === $scope.paragraph.id))) { - var scope = paragraphScope; + if (!data.noteId || data.noteId === noteId) { + var scope; + var registry; + + if (!data.paragraphId || data.paragraphId === $scope.paragraph.id) { + scope = paragraphScope; + registry = angularObjectRegistry; + } else { + var app = _.find($scope.apps, { id: data.paragraphId}); + if (app) { + scope = getAppScope(app); + registry = getAppRegistry(app); + } else { + // no matching app in this paragraph + return; + } + } + var varName = data.name; // clear watcher - if (angularObjectRegistry[varName]) { - angularObjectRegistry[varName].clearWatcher(); - angularObjectRegistry[varName] = undefined; + if (registry[varName]) { + registry[varName].clearWatcher(); + registry[varName] = undefined; } // remove scope variable @@ -365,12 +407,17 @@ angular.module('zeppelinWebApp') var newType = $scope.getResultType(data.paragraph); var oldGraphMode = $scope.getGraphMode(); var newGraphMode = $scope.getGraphMode(data.paragraph); + var oldActiveApp = _.get($scope.paragraph.config, 'helium.activeApp'); + var newActiveApp = _.get(data.paragraph.config, 'helium.activeApp'); + var resultRefreshed = (data.paragraph.dateFinished !== $scope.paragraph.dateFinished) || isEmpty(data.paragraph.result) !== isEmpty($scope.paragraph.result) || - data.paragraph.status === 'ERROR'; + data.paragraph.status === 'ERROR' || + (!newActiveApp && oldActiveApp !== newActiveApp); var statusChanged = (data.paragraph.status !== $scope.paragraph.status); + //console.log("updateParagraph oldData %o, newData %o. type %o -> %o, mode %o -> %o", $scope.paragraph, data, oldType, newType, oldGraphMode, newGraphMode); if ($scope.paragraph.text !== data.paragraph.text) { @@ -432,6 +479,14 @@ angular.module('zeppelinWebApp') $scope.renderText(); } + getApplicationStates(); + getSuggestions(); + + if (newActiveApp && newActiveApp !== oldActiveApp) { + var app = _.find($scope.apps, { id : newActiveApp }); + renderApp(app); + } + if (statusChanged || resultRefreshed) { // when last paragraph runs, zeppelin automatically appends new paragraph. // this broadcast will focus to the newly inserted paragraph @@ -1214,6 +1269,9 @@ angular.module('zeppelinWebApp') // graph options newConfig.graph.mode = newMode; + // see switchApp() + _.set(newConfig, 'helium.activeApp', undefined); + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); }; @@ -1432,7 +1490,8 @@ angular.module('zeppelinWebApp') }; $scope.isGraphMode = function(graphName) { - if ($scope.getResultType() === 'TABLE' && $scope.getGraphMode()===graphName) { + var activeAppId = _.get($scope.paragraph.config, 'helium.activeApp'); + if ($scope.getResultType() === 'TABLE' && $scope.getGraphMode()===graphName && !activeAppId) { return true; } else { return false; @@ -2180,4 +2239,188 @@ angular.module('zeppelinWebApp') } SaveAsService.SaveAs(dsv, 'data', extension); }; + + // Helium --------------------------------------------- + + // app states + $scope.apps = []; + + // suggested apps + $scope.suggestion = {}; + + $scope.switchApp = function(appId) { + var app = _.find($scope.apps, { id : appId }); + var config = $scope.paragraph.config; + var settings = $scope.paragraph.settings; + + var newConfig = angular.copy(config); + var newParams = angular.copy(settings.params); + + // 'helium.activeApp' can be cleared by setGraphMode() + _.set(newConfig, 'helium.activeApp', appId); + + commitConfig(newConfig, newParams); + }; + + $scope.loadApp = function(heliumPackage) { + var noteId = $route.current.pathParams.noteId; + $http.post(baseUrlSrv.getRestApiBase() + '/helium/load/' + noteId + '/' + $scope.paragraph.id, + heliumPackage) + .success(function(data, status, headers, config) { + console.log('Load app %o', data); + }) + .error(function(err, status, headers, config) { + console.log('Error %o', err); + }); + }; + + var commitConfig = function(config, params) { + var paragraph = $scope.paragraph; + commitParagraph(paragraph.title, paragraph.text, config, params); + }; + + var getApplicationStates = function() { + var appStates = []; + var paragraph = $scope.paragraph; + + // Display ApplicationState + if (paragraph.apps) { + _.forEach(paragraph.apps, function (app) { + appStates.push({ + id: app.id, + pkg: app.pkg, + status: app.status, + output: app.output + }); + }); + } + + // update or remove app states no longer exists + _.forEach($scope.apps, function(currentAppState, idx) { + var newAppState = _.find(appStates, { id : currentAppState.id }); + if (newAppState) { + angular.extend($scope.apps[idx], newAppState); + } else { + $scope.apps.splice(idx, 1); + } + }); + + // add new app states + _.forEach(appStates, function(app, idx) { + if ($scope.apps.length <= idx || $scope.apps[idx].id !== app.id) { + $scope.apps.splice(idx, 0, app); + } + }); + }; + + var getSuggestions = function() { + // Get suggested apps + var noteId = $route.current.pathParams.noteId; + $http.get(baseUrlSrv.getRestApiBase() + '/helium/suggest/' + noteId + '/' + $scope.paragraph.id) + .success(function(data, status, headers, config) { + console.log('Suggested apps %o', data); + $scope.suggestion = data.body; + }) + .error(function(err, status, headers, config) { + console.log('Error %o', err); + }); + }; + + var getAppScope = function(appState) { + if (!appState.scope) { + appState.scope = $rootScope.$new(true, $rootScope); + } + + return appState.scope; + }; + + var getAppRegistry = function(appState) { + if (!appState.registry) { + appState.registry = {}; + } + + return appState.registry; + }; + + var renderApp = function(appState) { + var retryRenderer = function() { + var targetEl = angular.element(document.getElementById('p' + appState.id)); + console.log('retry renderApp %o', targetEl); + if (targetEl.length) { + try { + console.log('renderApp %o', appState); + targetEl.html(appState.output); + $compile(targetEl.contents())(getAppScope(appState)); + } catch(err) { + console.log('App rendering error %o', err); + } + } else { + $timeout(retryRenderer, 1000); + } + }; + $timeout(retryRenderer); + }; + + $scope.$on('appendAppOutput', function(event, data) { + if ($scope.paragraph.id === data.paragraphId) { + var app = _.find($scope.apps, { id : data.appId }); + if (app) { + app.output += data.data; + + var paragraphAppState = _.find($scope.paragraph.apps, { id : data.appId }); + paragraphAppState.output = app.output; + + var targetEl = angular.element(document.getElementById('p' + app.id)); + targetEl.html(app.output); + $compile(targetEl.contents())(getAppScope(app)); + console.log('append app output %o', $scope.apps); + } + } + }); + + $scope.$on('updateAppOutput', function(event, data) { + if ($scope.paragraph.id === data.paragraphId) { + var app = _.find($scope.apps, { id : data.appId }); + if (app) { + app.output = data.data; + + var paragraphAppState = _.find($scope.paragraph.apps, { id : data.appId }); + paragraphAppState.output = app.output; + + var targetEl = angular.element(document.getElementById('p' + app.id)); + targetEl.html(app.output); + $compile(targetEl.contents())(getAppScope(app)); + console.log('append app output'); + } + } + }); + + $scope.$on('appLoad', function(event, data) { + if ($scope.paragraph.id === data.paragraphId) { + var app = _.find($scope.apps, {id: data.appId}); + if (!app) { + app = { + id: data.appId, + pkg: data.pkg, + status: 'UNLOADED', + output: '' + }; + + $scope.apps.push(app); + $scope.paragraph.apps.push(app); + $scope.switchApp(app.id); + } + } + }); + + $scope.$on('appStatusChange', function(event, data) { + if ($scope.paragraph.id === data.paragraphId) { + var app = _.find($scope.apps, {id: data.appId}); + if (app) { + app.status = data.status; + var paragraphAppState = _.find($scope.paragraph.apps, { id : data.appId }); + paragraphAppState.status = app.status; + } + } + }); }); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-web/src/app/notebook/paragraph/paragraph.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.css b/zeppelin-web/src/app/notebook/paragraph/paragraph.css index cea3ebd..d8b464e 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.css +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.css @@ -484,6 +484,11 @@ table.table-striped { right: 15px; } +.appSuggestion { + width: 200px; + padding: 5px 10px 5px 10px; +} + /* DSV download toggle button */ .caretBtn { padding-right: 4px !important; http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js index e07fb16..19afdc0 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js +++ b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js @@ -96,6 +96,14 @@ angular.module('zeppelinWebApp').factory('websocketEvents', function($rootScope, $rootScope.$broadcast('angularObjectUpdate', data); } else if (op === 'ANGULAR_OBJECT_REMOVE') { $rootScope.$broadcast('angularObjectRemove', data); + } else if (op === 'APP_APPEND_OUTPUT') { + $rootScope.$broadcast('appendAppOutput', data); + } else if (op === 'APP_UPDATE_OUTPUT') { + $rootScope.$broadcast('updateAppOutput', data); + } else if (op === 'APP_LOAD') { + $rootScope.$broadcast('appLoad', data); + } else if (op === 'APP_STATUS_CHANGE') { + $rootScope.$broadcast('appStatusChange', data); } }); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-web/test/spec/controllers/paragraph.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/test/spec/controllers/paragraph.js b/zeppelin-web/test/spec/controllers/paragraph.js index 7cdf748..77fc495 100644 --- a/zeppelin-web/test/spec/controllers/paragraph.js +++ b/zeppelin-web/test/spec/controllers/paragraph.js @@ -10,6 +10,13 @@ describe('Controller: ParagraphCtrl', function() { var paragraphMock = { config: {} }; + var route = { + current : { + pathParams : { + noteId : 'noteId' + } + } + }; beforeEach(inject(function($controller, $rootScope) { scope = $rootScope.$new(); @@ -18,8 +25,10 @@ describe('Controller: ParagraphCtrl', function() { ParagraphCtrl = $controller('ParagraphCtrl', { $scope: scope, websocketMsgSrv: websocketMsgSrvMock, - $element: {} + $element: {}, + $route: route }); + scope.init(paragraphMock); })); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java ---------------------------------------------------------------------- diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java index e2673df..6884622 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java @@ -362,6 +362,14 @@ public class ZeppelinConfiguration extends XMLConfiguration { return getRelativeDir(String.format("%s/interpreter.json", getConfDir())); } + public String getHeliumConfPath() { + return getRelativeDir(String.format("%s/helium.json", getConfDir())); + } + + public String getHeliumDefaultLocalRegistryPath() { + return getRelativeDir(ConfVars.ZEPPELIN_HELIUM_LOCALREGISTRY_DEFAULT); + } + public String getNotebookAuthorizationPath() { return getRelativeDir(String.format("%s/notebook-authorization.json", getConfDir())); } @@ -540,6 +548,7 @@ public class ZeppelinConfiguration extends XMLConfiguration { ZEPPELIN_NOTEBOOK_AUTO_INTERPRETER_BINDING("zeppelin.notebook.autoInterpreterBinding", true), ZEPPELIN_CONF_DIR("zeppelin.conf.dir", "conf"), ZEPPELIN_DEP_LOCALREPO("zeppelin.dep.localrepo", "local-repo"), + ZEPPELIN_HELIUM_LOCALREGISTRY_DEFAULT("zeppelin.helium.localregistry.default", "helium"), // Allows a way to specify a ',' separated list of allowed origins for rest and websockets // i.e. http://localhost:8080 ZEPPELIN_ALLOWED_ORIGINS("zeppelin.server.allowed.origins", "*"), http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9463fb85/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java ---------------------------------------------------------------------- diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java new file mode 100644 index 0000000..a07f5f0 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java @@ -0,0 +1,172 @@ +/* + * 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.helium; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.commons.io.FileUtils; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.notebook.Paragraph; +import org.apache.zeppelin.resource.DistributedResourcePool; +import org.apache.zeppelin.resource.ResourcePool; +import org.apache.zeppelin.resource.ResourcePoolUtils; +import org.apache.zeppelin.resource.ResourceSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Manages helium packages + */ +public class Helium { + Logger logger = LoggerFactory.getLogger(Helium.class); + private List<HeliumRegistry> registry = new LinkedList<HeliumRegistry>(); + + private final HeliumConf heliumConf; + private final String heliumConfPath; + private final String defaultLocalRegistryPath; + private final Gson gson; + + public Helium(String heliumConfPath, String defaultLocalRegistryPath) throws IOException { + this.heliumConfPath = heliumConfPath; + this.defaultLocalRegistryPath = defaultLocalRegistryPath; + + GsonBuilder builder = new GsonBuilder(); + builder.setPrettyPrinting(); + builder.registerTypeAdapter( + HeliumRegistry.class, new HeliumRegistrySerializer()); + gson = builder.create(); + + heliumConf = loadConf(heliumConfPath); + } + + /** + * Add HeliumRegistry + * + * @param registry + */ + public void addRegistry(HeliumRegistry registry) { + synchronized (this.registry) { + this.registry.add(registry); + } + } + + public List<HeliumRegistry> getAllRegistry() { + synchronized (this.registry) { + List list = new LinkedList<HeliumRegistry>(); + for (HeliumRegistry r : registry) { + list.add(r); + } + return list; + } + } + + private synchronized HeliumConf loadConf(String path) throws IOException { + File heliumConfFile = new File(path); + if (!heliumConfFile.isFile()) { + logger.warn("{} does not exists", path); + HeliumConf conf = new HeliumConf(); + LinkedList<HeliumRegistry> defaultRegistry = new LinkedList<HeliumRegistry>(); + defaultRegistry.add(new HeliumLocalRegistry("local", defaultLocalRegistryPath)); + conf.setRegistry(defaultRegistry); + this.registry = conf.getRegistry(); + return conf; + } else { + String jsonString = FileUtils.readFileToString(heliumConfFile); + HeliumConf conf = gson.fromJson(jsonString, HeliumConf.class); + this.registry = conf.getRegistry(); + return conf; + } + } + + public synchronized void save() throws IOException { + String jsonString; + synchronized (registry) { + heliumConf.setRegistry(registry); + jsonString = gson.toJson(heliumConf); + } + + File heliumConfFile = new File(heliumConfPath); + if (!heliumConfFile.exists()) { + heliumConfFile.createNewFile(); + } + + FileUtils.writeStringToFile(heliumConfFile, jsonString); + } + + public List<HeliumPackageSearchResult> getAllPackageInfo() { + List<HeliumPackageSearchResult> list = new LinkedList<HeliumPackageSearchResult>(); + synchronized (registry) { + for (HeliumRegistry r : registry) { + try { + for (HeliumPackage pkg : r.getAll()) { + list.add(new HeliumPackageSearchResult(r.name(), pkg)); + } + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + } + return list; + } + + public HeliumPackageSuggestion suggestApp(Paragraph paragraph) { + HeliumPackageSuggestion suggestion = new HeliumPackageSuggestion(); + + Interpreter intp = paragraph.getCurrentRepl(); + if (intp == null) { + return suggestion; + } + + ResourcePool resourcePool = intp.getInterpreterGroup().getResourcePool(); + ResourceSet allResources; + + if (resourcePool != null) { + if (resourcePool instanceof DistributedResourcePool) { + allResources = ((DistributedResourcePool) resourcePool).getAll(true); + } else { + allResources = resourcePool.getAll(); + } + } else { + allResources = ResourcePoolUtils.getAllResources(); + } + + for (HeliumPackageSearchResult pkg : getAllPackageInfo()) { + ResourceSet resources = ApplicationLoader.findRequiredResourceSet( + pkg.getPkg().getResources(), + paragraph.getNote().getId(), + paragraph.getId(), + allResources); + if (resources == null) { + continue; + } else { + suggestion.addAvailablePackage(pkg); + } + } + + suggestion.sort(); + return suggestion; + } +}
